/*-------------------------------------------------------------------------
 *
 * procarray.c
 *	  POSTGRES 进程数组代码。
 *
 *
 * 这个模块维护 PGPROC 子结构的数组，以及 ProcGlobal 中与所有活动后台相关的
 * 数组。虽然这个模块有几种用途，但主要用途是确定当前正在运行的事务集合。
 *
 * 由于各种细微的竞争条件，后台在设置或清除其 xid（在 ProcGlobal->xids[]/
 * MyProc->xid 中）时保持正确的锁定是至关重要的。请参阅 src/backend/access/
 * transam/README 中的注释。
 *
 * 进程数组现在还包括表示已准备事务的结构。它们的 xid 和 subxids 字段是有效的，
 * myProcLocks 列表也是。通过检查 pid == 0，可以在需要时将其与常规后台 PGPROCS
 * 区分开来。
 *
 * 在热备模式中，我们还保留一个 XIDs 列表，表示在主节点上正在运行的已知事务（更
 * 准确地说是截至 WAL 流的当前点时正在运行的事务）。这个列表保存在 KnownAssignedXids
 * 数组中，并通过观察到达的 XIDs 的序列进行更新。这是必要的，因为如果我们在
 * 进行备份查询时将这些 XIDs 排除在快照外，它们将显示为已完成，从而导致 MVCC 失败。
 * 注意，在热备模式中，PGPROC 数组表示备用进程，根据定义不在运行具有 XIDs 的事务。
 *
 * 主节点上的后台可能会在未为其事务写入中止记录的情况下终止。虽然这不应该发生，
 * 但这会无限期地占用 KnownAssignedXids，因此我们通过在到达有效的运行中 XIDs 
 * 列表时修剪数组来保护自己。
 *
 * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
 * Portions Copyright (c) 1994, Regents of the University of California
 *
 *
 * IDENTIFICATION
 *	  src/backend/storage/ipc/procarray.c
 *
 *-------------------------------------------------------------------------
 */
#include "postgres.h"

#include <signal.h>

#include "access/clog.h"
#include "access/subtrans.h"
#include "access/transam.h"
#include "access/twophase.h"
#include "access/xact.h"
#include "access/xlogutils.h"
#include "catalog/catalog.h"
#include "catalog/pg_authid.h"
#include "commands/dbcommands.h"
#include "miscadmin.h"
#include "pgstat.h"
#include "storage/proc.h"
#include "storage/procarray.h"
#include "storage/spin.h"
#include "utils/acl.h"
#include "utils/builtins.h"
#include "utils/rel.h"
#include "utils/snapmgr.h"

#define UINT32_ACCESS_ONCE(var)		 ((uint32)(*((volatile uint32 *)&(var))))

/* 我们的共享内存区域 */
typedef struct ProcArrayStruct
{
	int			numProcs;		/* 有效进程条目的数量 */
	int			maxProcs;		/* 进程数组的分配大小 */

	/*
	 * 已知分配的 XID 处理
	 */
	int			maxKnownAssignedXids;	/* 数组的分配大小 */
	int			numKnownAssignedXids;	/* 当前有效条目的数量 */
	int			tailKnownAssignedXids;	/* 最旧有效元素的索引 */
	int			headKnownAssignedXids;	/* 最新元素的索引，+ 1 */
	slock_t		known_assigned_xids_lck;	/* 保护头/尾指针 */

	/*
	 * 已从 KnownAssignedXids 数组中移除的最高 subxid，以防止溢出；如果没有则为 InvalidTransactionId。我们追踪这个是出于
	 * 类似于在 PGPROC 条目中追踪溢出缓存 subxids 的原因。要更改此项必须持有独占的 ProcArrayLock，读取时持有共享锁。
	 */
	TransactionId lastOverflowedXid;

	/* 任何复制槽的最旧 xmin */
	TransactionId replication_slot_xmin;
	/* 任何复制槽的最旧目录 xmin */
	TransactionId replication_slot_catalog_xmin;

	/* 索引到 allProcs[]，具有 PROCARRAY_MAXPROCS 项 */
	int			pgprocnos[FLEXIBLE_ARRAY_MEMBER];
} ProcArrayStruct;

/*
 * GlobalVisTest* 函数族的状态。这些函数可用于决定一个已删除的行是否可以在不违反 MVCC 语义的情况下被移除：
 * 如果已删除行的 xmax 被认为没有任何人正在运行，则可以移除该行。
 *
 * 为了避免减慢 GetSnapshotData() 的速度，我们在构建快照时不计算精确的截止 XID（关注频繁变化的
 * xmins 性能较差）。相反，我们在构建快照时计算两个边界：
 *
 * 1) definitely_needed，表示被 XIDs >=
 *    definitely_needed 删除的行肯定仍然可见。
 *
 * 2) maybe_needed，表示被 XIDs < maybe_needed 删除的行可以
 *    肯定地被移除。
 *
 * 当测试一个在两个边界之间的 XID（即 XID >= maybe_needed
 * && XID < definitely_needed）时，可以重新计算边界（使用
 * ComputeXidHorizons()）以获得更准确的答案。这比
 * 始终保持一个准确值要便宜。
 *
 * 由于计算准确边界的成本不低，因此我们限制在短时间内发生的次数。请参见 GlobalVisTestShouldUpdate()。
 *
 *
 * 这个结构有三个后端生命周期实例，针对不同类型的关系进行了优化。因为例如一个数据库中的普通用户定义表对连接到另一个数据库的
 * 后端是不可访问的，因此一个特定于关系的测试可以比对共享关系的测试更加激进。当前我们追踪四种不同的状态：
 *
 * 1) GlobalVisSharedRels，仅当任何数据库中的快照、复制槽的 xmin 和复制槽的 catalog_xmin 都不再认为
 *    XID 正在运行时，才考虑 XID 的影响对每个人可见。
 *
 * 2) GlobalVisCatalogRels，仅当当前数据库中的快照、复制槽的 xmin 和复制槽的 catalog_xmin 都不再认为
 *    XID 正在运行时，才考虑 XID 的影响对每个人可见。
 *
 *    即，与 GlobalVisSharedRels 的区别在于忽略了其他数据库中的快照。
 *
 * 3) GlobalVisDataRels，仅当当前数据库中的快照和复制槽的 xmin 都不再认为 XID 正在运行时，才考虑
 *    XID 的影响对每个人可见。
 *
 *    即，与 GlobalVisCatalogRels 的区别在于不考虑复制槽的 catalog_xmin。
 *
 * 4) GlobalVisTempRels，仅考虑当前会话，因为临时表对其他会话不可见。
 *
 * GlobalVisTestFor(relation) 返回适用于该关系的状态。
 *
 * 边界是 FullTransactionIds 而不是 TransactionIds，以避免回绕危险。否则将不存在 procarray 状态来防止
 * maybe_needed 在 GetSnapshotData() 调用后变得足够老。
 *
 * typedef 在头文件中。
 */
struct GlobalVisState
{
	/* XIDs >= 被认为是某个后端正在运行 */
	FullTransactionId definitely_needed;

	/* XIDs < 不被认为是任何后端正在运行 */
	FullTransactionId maybe_needed;
};

/*
 * ComputeXidHorizons() 的结果。
 */
typedef struct ComputeXidHorizonsResult
{
	/*
	 * ComputeXidHorizons() 持有 ProcArrayLock 时 ShmemVariableCache->latestCompletedXid 的值。
	 */
	FullTransactionId latest_completed;

	/*
	 * procArray->replication_slot_xmin 和
	 * procArray->replication_slot_catalog_xmin 的相同值。
	 */
	TransactionId slot_xmin;
	TransactionId slot_catalog_xmin;

	/*
	 * 任何后端仍可能认为正在运行的最旧 xid。需要包括正在运行 VACUUM 的进程，这与正常的可见性
	 * 截断不同，因为清理需要能够在确定可见性时执行 pg_subtrans 查询，但不关心在其 xmin 上方的
	 * 被删除行。
	 *
	 * 这可能只需要用于确定是否可以截断 pg_subtrans。目前它包括复制槽的影响，出于历史原因。但这可能会被更改。
	 */
	TransactionId oldest_considered_running;

	/*
	 * 需要在共享表中保留的已删除元组的最旧 xid。
	 *
	 * 这包括复制槽的影响。如果不希望这样，请查看 shared_oldest_nonremovable_raw；
	 */
	TransactionId shared_oldest_nonremovable;

	
/*
	 * 可能需要在共享表中保留的最旧 xid。这与 shared_oldest_nonremovable 相同，  
	 * 除了它不受复制槽的 catalog_xmin 影响。
	 *
	 * 这主要用于能够通过 hot_standby_feedback 将 catalog_xmin 发送到上游  
	 * 流复制服务器，以便它们在访问 catalog 表时能应用限制。
	 */
	TransactionId shared_oldest_nonremovable_raw;

	/*
	 * 在非共享 catalog 表中需要保留已删除元组的最旧 xid。
	 */
	TransactionId catalog_oldest_nonremovable;

	/*
	 * 在普通用户定义表中需要保留已删除元组的最旧 xid。
	 */
	TransactionId data_oldest_nonremovable;

	/*
	 * 在该会话的临时表中需要保留已删除元组的最旧 xid。
	 */
	TransactionId temp_oldest_nonremovable;
} ComputeXidHorizonsResult;

/*
 * GlobalVisHorizonKindForRel() 的返回值。
 */
typedef enum GlobalVisHorizonKind
{
	VISHORIZON_SHARED,
	VISHORIZON_CATALOG,
	VISHORIZON_DATA,
	VISHORIZON_TEMP
} GlobalVisHorizonKind;

/*
 * KnownAssignedXidsCompress() 的原因代码。
 */
typedef enum KAXCompressReason
{
	KAX_NO_SPACE,				/* 需要在数组末尾释放空间 */
	KAX_PRUNE,					/* 我们刚刚修剪了旧条目 */
	KAX_TRANSACTION_END,		/* 我们刚刚提交/删除了一些 XIDs */
	KAX_STARTUP_PROCESS_IDLE	/* 启动进程即将进入休眠状态 */
} KAXCompressReason;


static ProcArrayStruct *procArray;

static PGPROC *allProcs;

/*
 * 缓存以减少对 TransactionIdIsInProgress() 重复调用的开销
 */
static TransactionId cachedXidIsNotInProgress = InvalidTransactionId;

/*
 * 用于跟踪恢复中模拟事务的簿记
 */
static TransactionId *KnownAssignedXids;
static bool *KnownAssignedXidsValid;
static TransactionId latestObservedXid = InvalidTransactionId;

/*
 * 如果我们处于 STANDBY_SNAPSHOT_PENDING 状态，standbySnapshotPendingXmin 是  
	 * 可能仍在运行的最高 xid，该 xid 在 KnownAssignedXids 中没有。
	 */
static TransactionId standbySnapshotPendingXmin;

/*
 * 用于不同类型关系的可见性检查的状态。有关详细信息，请查看 struct  
	 * GlobalVisState。由于共享、目录、普通和临时关系可以具有不同的视野，
	 * 因此每种状态都有一个这样的存在。
	 */
static GlobalVisState GlobalVisSharedRels;
static GlobalVisState GlobalVisCatalogRels;
static GlobalVisState GlobalVisDataRels;
static GlobalVisState GlobalVisTempRels;

/*
 * 该后端的 RecentXmin 在上次准确的 xmin 视野被重新计算时，  
	 * 如果未计算，则为 InvalidTransactionId。用于限制准确视野 
	 * 被重新计算的次数。请参见 GlobalVisTestShouldUpdate()。
	 */
static TransactionId ComputeXidHorizonsResultLastXmin;

#ifdef XIDCACHE_DEBUG

/* XidCache 测量的计数器 */
static long xc_by_recent_xmin = 0;
static long xc_by_known_xact = 0;
static long xc_by_my_xact = 0;
static long xc_by_latest_xid = 0;
static long xc_by_main_xid = 0;
static long xc_by_child_xid = 0;
static long xc_by_known_assigned = 0;
static long xc_no_overflow = 0;
static long xc_slow_answer = 0;

#define xc_by_recent_xmin_inc()		(xc_by_recent_xmin++)
#define xc_by_known_xact_inc()		(xc_by_known_xact++)
#define xc_by_my_xact_inc()			(xc_by_my_xact++)
#define xc_by_latest_xid_inc()		(xc_by_latest_xid++)
#define xc_by_main_xid_inc()		(xc_by_main_xid++)
#define xc_by_child_xid_inc()		(xc_by_child_xid++)
#define xc_by_known_assigned_inc()	(xc_by_known_assigned++)
#define xc_no_overflow_inc()		(xc_no_overflow++)
#define xc_slow_answer_inc()		(xc_slow_answer++)

static void fc_DisplayXidCache(void);
#else							/* !XIDCACHE_DEBUG */

#define xc_by_recent_xmin_inc()		((void) 0)
#define xc_by_known_xact_inc()		((void) 0)
#define xc_by_my_xact_inc()			((void) 0)
#define xc_by_latest_xid_inc()		((void) 0)
#define xc_by_main_xid_inc()		((void) 0)
#define xc_by_child_xid_inc()		((void) 0)
#define xc_by_known_assigned_inc()	((void) 0)
#define xc_no_overflow_inc()		((void) 0)
#define xc_slow_answer_inc()		((void) 0)
#endif							/* XIDCACHE_DEBUG */

/* 用于处理备用的 KnownAssignedXids 数组的原始数据 */
static void fc_KnownAssignedXidsCompress(KAXCompressReason fc_reason, bool fc_haveLock);
static void fc_KnownAssignedXidsAdd(TransactionId fc_from_xid, TransactionId fc_to_xid,
								 bool fc_exclusive_lock);
static bool fc_KnownAssignedXidsSearch(TransactionId fc_xid, bool fc_remove);
static bool fc_KnownAssignedXidExists(TransactionId fc_xid);
static void fc_KnownAssignedXidsRemove(TransactionId fc_xid);
static void fc_KnownAssignedXidsRemoveTree(TransactionId fc_xid, int fc_nsubxids,
										TransactionId *fc_subxids);
static void fc_KnownAssignedXidsRemovePreceding(TransactionId fc_xid);
static int	fc_KnownAssignedXidsGet(TransactionId *fc_xarray, TransactionId fc_xmax);
static int	fc_KnownAssignedXidsGetAndSetXmin(TransactionId *fc_xarray,
										   TransactionId *fc_xmin,
										   TransactionId fc_xmax);
static TransactionId fc_KnownAssignedXidsGetOldestXmin(void);
static void fc_KnownAssignedXidsDisplay(int fc_trace_level);
static void fc_KnownAssignedXidsReset(void);
static inline void fc_ProcArrayEndTransactionInternal(PGPROC *fc_proc, TransactionId fc_latestXid);
static void fc_ProcArrayGroupClearXid(PGPROC *fc_proc, TransactionId fc_latestXid);
static void fc_MaintainLatestCompletedXid(TransactionId fc_latestXid);
static void fc_MaintainLatestCompletedXidRecovery(TransactionId fc_latestXid);
static void fc_TransactionIdRetreatSafely(TransactionId *fc_xid,
									   int fc_retreat_by,
									   FullTransactionId fc_rel);

static inline FullTransactionId fc_FullXidRelativeTo(FullTransactionId fc_rel,
												  TransactionId fc_xid);
static void fc_GlobalVisUpdateApply(ComputeXidHorizonsResult *fc_horizons);

/* 
 * 报告 CreateSharedProcArray 所需的共享内存空间。
 */
Size ProcArrayShmemSize(void)
{
	Size		fc_size;

	/* ProcArray 结构本身的大小 */
#define PROCARRAY_MAXPROCS	(MaxBackends + max_prepared_xacts)

	fc_size = offsetof(ProcArrayStruct, pgprocnos);
	fc_size = add_size(fc_size, mul_size(sizeof(int), PROCARRAY_MAXPROCS));

	/* 
	 * 在热备处理过程中，我们有一个名为 
	 * KnownAssignedXids 的数据结构，在共享内存中创建。 
	 * 在 GetSnapshotData()、TransactionIdIsInProgress() 和 
	 * GetRunningTransactionData() 期间，本地数据结构也会在 
	 * 各种后端中创建。在这些函数中创建的所有主要结构必须 
	 * 大小相同，因为我们有时可能会复制整个数据结构。 
	 * 我们将此大小称为 TOTAL_MAX_CACHED_SUBXIDS。 
	 * 
	 * 理想情况下，我们只在实际进行热备的情况下创建此结构，但我们 
	 * 在设置共享内存时尚不知道这一点。 
	 */
#define TOTAL_MAX_CACHED_SUBXIDS \
	((PGPROC_MAX_CACHED_SUBXIDS + 1) * PROCARRAY_MAXPROCS)

	if (EnableHotStandby)
	{
		fc_size = add_size(fc_size,
						mul_size(sizeof(TransactionId),
								 TOTAL_MAX_CACHED_SUBXIDS));
		fc_size = add_size(fc_size,
						mul_size(sizeof(bool), TOTAL_MAX_CACHED_SUBXIDS));
	}

	return fc_size;
}

/* 
 * 在主进程启动期间初始化共享 PGPROC 数组。
 */
void CreateSharedProcArray(void)
{
	bool		fc_found;

	/* 创建或附加到 ProcArray 共享结构 */
	procArray = (ProcArrayStruct *)
		ShmemInitStruct("Proc Array",
						add_size(offsetof(ProcArrayStruct, pgprocnos),
								 mul_size(sizeof(int),
										  PROCARRAY_MAXPROCS)),
						&fc_found);

	if (!fc_found)
	{
		/*
		 * 我们是第一个 - 初始化。
		 */
		procArray->numProcs = 0;
		procArray->maxProcs = PROCARRAY_MAXPROCS;
		procArray->maxKnownAssignedXids = TOTAL_MAX_CACHED_SUBXIDS;
		procArray->numKnownAssignedXids = 0;
		procArray->tailKnownAssignedXids = 0;
		procArray->headKnownAssignedXids = 0;
		SpinLockInit(&procArray->known_assigned_xids_lck);
		procArray->lastOverflowedXid = InvalidTransactionId;
		procArray->replication_slot_xmin = InvalidTransactionId;
		procArray->replication_slot_catalog_xmin = InvalidTransactionId;
		ShmemVariableCache->xactCompletionCount = 1;
	}

	allProcs = ProcGlobal->allProcs;

	/* 如果需要，也创建或附加到 KnownAssignedXids 数组 */
	if (EnableHotStandby)
	{
		KnownAssignedXids = (TransactionId *)
			ShmemInitStruct("KnownAssignedXids",
							mul_size(sizeof(TransactionId),
									 TOTAL_MAX_CACHED_SUBXIDS),
							&fc_found);
		KnownAssignedXidsValid = (bool *)
			ShmemInitStruct("KnownAssignedXidsValid",
							mul_size(sizeof(bool), TOTAL_MAX_CACHED_SUBXIDS),
							&fc_found);
	}
}

/* 
 * 将指定的 PGPROC 添加到共享数组中。
 */
void ProcArrayAdd(PGPROC *fc_proc)
{
	ProcArrayStruct *fc_arrayP = procArray;
	int			fc_index;
	int			fc_movecount;

	/* 请参见 ProcGlobal 注释，解释为何同时持有两个锁 */
	LWLockAcquire(ProcArrayLock, LW_EXCLUSIVE);
	LWLockAcquire(XidGenLock, LW_EXCLUSIVE);

	if (fc_arrayP->numProcs >= fc_arrayP->maxProcs)
	{
		/* 
		 * 哎呀，没有空间。 （这真的不应该发生，因为也有 
		 * 固定数量的 PGPROC 结构，因此我们应该更早失败。）
		 */
		ereport(FATAL,
				(errcode(ERRCODE_TOO_MANY_CONNECTIONS),
				 errmsg("sorry, too many clients already")));
	}

	/* 
	 * 保持 procs 数组按 (PGPROC *) 排序，以便我们可以更好地利用 
	 * 引用的局部性。这在遍历 ProcArray 时很有用，因为在缓存中 
	 * 找到下一个 PGPROC 结构的可能性增加。 
	 * 
	 * 由于添加/删除一个 proc 的发生频率远低于对 ProcArray 
	 * 本身的访问，因此开销应该是微不足道的 
	 */
	for (fc_index = 0; fc_index < fc_arrayP->numProcs; fc_index++)
	{
		int			fc_procno PG_USED_FOR_ASSERTS_ONLY = fc_arrayP->pgprocnos[fc_index];

		Assert(fc_procno >= 0 && fc_procno < (fc_arrayP->maxProcs + NUM_AUXILIARY_PROCS));
		Assert(allProcs[fc_procno].pgxactoff == fc_index);

		/* 如果我们找到了数组中的正确位置，退出 */
		if (fc_arrayP->pgprocnos[fc_index] > fc_proc->pgprocno)
			break;
	}

	fc_movecount = fc_arrayP->numProcs - fc_index;
	memmove(&fc_arrayP->pgprocnos[fc_index + 1],
			&fc_arrayP->pgprocnos[fc_index],
			fc_movecount * sizeof(*fc_arrayP->pgprocnos));
	memmove(&ProcGlobal->xids[fc_index + 1],
			&ProcGlobal->xids[fc_index],
			fc_movecount * sizeof(*ProcGlobal->xids));
	memmove(&ProcGlobal->subxidStates[fc_index + 1],
			&ProcGlobal->subxidStates[fc_index],
			fc_movecount * sizeof(*ProcGlobal->subxidStates));
	memmove(&ProcGlobal->statusFlags[fc_index + 1],
			&ProcGlobal->statusFlags[fc_index],
			fc_movecount * sizeof(*ProcGlobal->statusFlags));

	fc_arrayP->pgprocnos[fc_index] = fc_proc->pgprocno;
	fc_proc->pgxactoff = fc_index;
	ProcGlobal->xids[fc_index] = fc_proc->xid;
	ProcGlobal->subxidStates[fc_index] = fc_proc->subxidStatus;
	ProcGlobal->statusFlags[fc_index] = fc_proc->statusFlags;

	fc_arrayP->numProcs++;

	/* 为所有后续 PGPROCs 调整 pgxactoff */
	fc_index++;
	for (; fc_index < fc_arrayP->numProcs; fc_index++)
	{
		int			fc_procno = fc_arrayP->pgprocnos[fc_index];

		Assert(fc_procno >= 0 && fc_procno < (fc_arrayP->maxProcs + NUM_AUXILIARY_PROCS));
		Assert(allProcs[fc_procno].pgxactoff == fc_index - 1);

		allProcs[fc_procno].pgxactoff = fc_index;
	}

	/* 
	 * 以反向获取顺序释放，以减少在持有 ProcArrayLock 
	 * 时等待 XidGenLock 的频率。
	 */
	LWLockRelease(XidGenLock);
	LWLockRelease(ProcArrayLock);
}

/* 
 * 从共享数组中移除指定的 PGPROC。
 * 
 * 当 latestXid 是有效的 XID 时，我们正在从数组中移除一个 
 * 活动的 2PC gxact，从而使其看起来不再处于“运行”状态。 
 * 在这种情况下，我们必须推进 latestCompletedXid。 
 * （这实际上与 ProcArrayEndTransaction 随后移除 PGPROC 相同，
 * 但我们只获取 ProcArrayLock 一次，并且不损坏 PGPROC 的内容；
 * twophase.c 依赖于后者。）
 */
void ProcArrayRemove(PGPROC *fc_proc, TransactionId fc_latestXid)
{
	ProcArrayStruct *fc_arrayP = procArray;
	int			fc_myoff;
	int			fc_movecount;

#ifdef XIDCACHE_DEBUG
	/* 在后端关闭时转储统计信息，但不是准备事务结束时 */
	if (fc_proc->pid != 0)
		fc_DisplayXidCache();
#endif

	/* 请参见 ProcGlobal 注释，解释为何同时持有两个锁 */
	LWLockAcquire(ProcArrayLock, LW_EXCLUSIVE);
	LWLockAcquire(XidGenLock, LW_EXCLUSIVE);

	fc_myoff = fc_proc->pgxactoff;

	Assert(fc_myoff >= 0 && fc_myoff < fc_arrayP->numProcs);
	Assert(ProcGlobal->allProcs[fc_arrayP->pgprocnos[fc_myoff]].pgxactoff == fc_myoff);

	if (TransactionIdIsValid(fc_latestXid))
	{
		Assert(TransactionIdIsValid(ProcGlobal->xids[fc_myoff]));

		/* 在持有锁时推进全局 latestCompletedXid */
		fc_MaintainLatestCompletedXid(fc_latestXid);

		/* xactCompletionCount 也一样 */
		ShmemVariableCache->xactCompletionCount++;

		ProcGlobal->xids[fc_myoff] = InvalidTransactionId;
		ProcGlobal->subxidStates[fc_myoff].overflowed = false;
		ProcGlobal->subxidStates[fc_myoff].count = 0;
	}
	else
	{
		/* 不应该试图在这里移除一个活动事务 */
		Assert(!TransactionIdIsValid(ProcGlobal->xids[fc_myoff]));
	}

	Assert(!TransactionIdIsValid(ProcGlobal->xids[fc_myoff]));
	Assert(ProcGlobal->subxidStates[fc_myoff].count == 0);
	Assert(ProcGlobal->subxidStates[fc_myoff].overflowed == false);

	ProcGlobal->statusFlags[fc_myoff] = 0;

	/* 保持 PGPROC 数组排序。参见上面的说明 */
	fc_movecount = fc_arrayP->numProcs - fc_myoff - 1;
	memmove(&fc_arrayP->pgprocnos[fc_myoff],
			&fc_arrayP->pgprocnos[fc_myoff + 1],
			fc_movecount * sizeof(*fc_arrayP->pgprocnos));
	memmove(&ProcGlobal->xids[fc_myoff],
			&ProcGlobal->xids[fc_myoff + 1],
			fc_movecount * sizeof(*ProcGlobal->xids));
	memmove(&ProcGlobal->subxidStates[fc_myoff],
			&ProcGlobal->subxidStates[fc_myoff + 1],
			fc_movecount * sizeof(*ProcGlobal->subxidStates));
	memmove(&ProcGlobal->statusFlags[fc_myoff],
			&ProcGlobal->statusFlags[fc_myoff + 1],
			fc_movecount * sizeof(*ProcGlobal->statusFlags));

	fc_arrayP->pgprocnos[fc_arrayP->numProcs - 1] = -1;	/* 供调试使用 */
	fc_arrayP->numProcs--;

	/* 
	 * 为已移除的 PGPROC 调整后续 procs 的 pgxactoff（注意 
	 * numProcs 已经递减）。
	 */
	for (int fc_index = fc_myoff; fc_index < fc_arrayP->numProcs; fc_index++)
	{
		int			fc_procno = fc_arrayP->pgprocnos[fc_index];

		Assert(fc_procno >= 0 && fc_procno < (fc_arrayP->maxProcs + NUM_AUXILIARY_PROCS));
		Assert(allProcs[fc_procno].pgxactoff - 1 == fc_index);

		allProcs[fc_procno].pgxactoff = fc_index;
	}

	/* 
	 * 以反向获取顺序释放，以减少在持有 ProcArrayLock 
	 * 时等待 XidGenLock 的频率。
	 */
	LWLockRelease(XidGenLock);
	LWLockRelease(ProcArrayLock);
}


/* 
 * ProcArrayEndTransaction -- 将事务标记为不再运行 
 * 
 * 这在提交和中止情况下交替使用。事务的 
 * 提交/中止必须已经报告给 WAL 和 pg_xact。 
 * 
 * proc 目前总是 MyProc，但我们明确传递它以便于灵活性。 
 * latestXid 是事务的主 XID 和子事务之间的最新 Xid， 
 * 如果没有 XID，则为 InvalidTransactionId。 
 * （我们必须要求调用者传递 latestXid，而不是从 
 * PGPROC 的内容中计算它，因为 PGPROC 中的 subxid 
 * 信息可能是不完整的。）
 */
void ProcArrayEndTransaction(PGPROC *fc_proc, TransactionId fc_latestXid)
{
	if (TransactionIdIsValid(fc_latestXid))
	{
		/* 
		 * 在清除我们声明的 XID 时必须锁定 ProcArrayLock，以便 
		 * 在其他人拍摄快照时我们不会退出“运行”事务的集合。 
		 * 请参见 src/backend/access/transam/README 中的讨论。
		 */
		Assert(TransactionIdIsValid(fc_proc->xid));

		/* 
		 * 如果我们可以立即获取 ProcArrayLock，我们清除自己的 
		 * XID 并释放锁。如果不能，则使用组 XID 清除以提高 
		 * 效率。
		 */
		if (LWLockConditionalAcquire(ProcArrayLock, LW_EXCLUSIVE))
		{
			fc_ProcArrayEndTransactionInternal(fc_proc, fc_latestXid);
			LWLockRelease(ProcArrayLock);
		}
		else
			fc_ProcArrayGroupClearXid(fc_proc, fc_latestXid);
	}
	else
	{
		/* 
		 * 如果我们没有 XID，我们不需要锁定，因为我们不会影响 
		 * 其他人对快照的计算。我们可能会改变他们对全局 xmin 的 
		 * 估计，但那没关系。
		 */
		Assert(!TransactionIdIsValid(fc_proc->xid));
		Assert(fc_proc->subxidStatus.count == 0);
		Assert(!fc_proc->subxidStatus.overflowed);

		fc_proc->lxid = InvalidLocalTransactionId;
		fc_proc->xmin = InvalidTransactionId;

		/* 确保在中止时清除此项 */
		fc_proc->delayChkptFlags = 0;

		fc_proc->recoveryConflictPending = false;

		/* 必须与 xid/xmin 一起清除： */
		/* 避免不必要地污染共享缓存行 */
		if (fc_proc->statusFlags & PROC_VACUUM_STATE_MASK)
		{
			Assert(!LWLockHeldByMe(ProcArrayLock));
			LWLockAcquire(ProcArrayLock, LW_EXCLUSIVE);
			Assert(fc_proc->statusFlags == ProcGlobal->statusFlags[fc_proc->pgxactoff]);
			fc_proc->statusFlags &= ~PROC_VACUUM_STATE_MASK;
			ProcGlobal->statusFlags[fc_proc->pgxactoff] = fc_proc->statusFlags;
			LWLockRelease(ProcArrayLock);
		}
	}
}

/*
 * 标记写事务为不再运行。
 *
 * 我们在这里不进行任何锁定；调用者必须处理这个问题。
 */
static inline void fc_ProcArrayEndTransactionInternal(PGPROC *fc_proc, TransactionId fc_latestXid)
{
	int			fc_pgxactoff = fc_proc->pgxactoff;

	/*
	 * 注意：我们在这里需要独占锁，因为我们将要改变其他
	 * 进程的 PGPROC 条目。
	 */
	Assert(LWLockHeldByMeInMode(ProcArrayLock, LW_EXCLUSIVE));
	Assert(TransactionIdIsValid(ProcGlobal->xids[fc_pgxactoff]));
	Assert(ProcGlobal->xids[fc_pgxactoff] == fc_proc->xid);

	ProcGlobal->xids[fc_pgxactoff] = InvalidTransactionId;
	fc_proc->xid = InvalidTransactionId;
	fc_proc->lxid = InvalidLocalTransactionId;
	fc_proc->xmin = InvalidTransactionId;

	/* 确保在中止时清除此项 */
	fc_proc->delayChkptFlags = 0;

	fc_proc->recoveryConflictPending = false;

	/* 必须与 xid/xmin 一起清除： */
	/* 避免不必要地污染共享缓存行 */
	if (fc_proc->statusFlags & PROC_VACUUM_STATE_MASK)
	{
		fc_proc->statusFlags &= ~PROC_VACUUM_STATE_MASK;
		ProcGlobal->statusFlags[fc_proc->pgxactoff] = fc_proc->statusFlags;
	}

	/* 在持有锁的同时清除子事务-XID 缓存 */
	Assert(ProcGlobal->subxidStates[fc_pgxactoff].count == fc_proc->subxidStatus.count &&
		   ProcGlobal->subxidStates[fc_pgxactoff].overflowed == fc_proc->subxidStatus.overflowed);
	if (fc_proc->subxidStatus.count > 0 || fc_proc->subxidStatus.overflowed)
	{
		ProcGlobal->subxidStates[fc_pgxactoff].count = 0;
		ProcGlobal->subxidStates[fc_pgxactoff].overflowed = false;
		fc_proc->subxidStatus.count = 0;
		fc_proc->subxidStatus.overflowed = false;
	}

	/* 在持有锁的同时也要推进全局 latestCompletedXid */
	fc_MaintainLatestCompletedXid(fc_latestXid);

	/* xactCompletionCount 也一样 */
	ShmemVariableCache->xactCompletionCount++;
}

/*
 * ProcArrayGroupClearXid -- 组 XID 清除
 *
 * 当我们无法立即以独占模式获取 ProcArrayLock 在
 * 提交时，将自己添加到需要清除 XID 的进程列表中。第一个将自己
 * 添加到列表的进程将以独占模式获取 ProcArrayLock，并代表所有组
 * 成员执行 ProcArrayEndTransactionInternal。这避免了当许多进程同时试图
 * 提交时在 ProcArrayLock 周围的巨大竞争，因为锁不需要反复从一个
 * 提交的进程移交到下一个。
 */
static void fc_ProcArrayGroupClearXid(PGPROC *fc_proc, TransactionId fc_latestXid)
{
	PROC_HDR   *fc_procglobal = ProcGlobal;
	uint32		fc_nextidx;
	uint32		fc_wakeidx;

	/* 我们肯定应该有一个 XID 需要清除。 */
	Assert(TransactionIdIsValid(fc_proc->xid));

	/* 将自己添加到需要组 XID 清除的进程列表中。 */
	fc_proc->procArrayGroupMember = true;
	fc_proc->procArrayGroupMemberXid = fc_latestXid;
	fc_nextidx = pg_atomic_read_u32(&fc_procglobal->procArrayGroupFirst);
	while (true)
	{
		pg_atomic_write_u32(&fc_proc->procArrayGroupNext, fc_nextidx);

		if (pg_atomic_compare_exchange_u32(&fc_procglobal->procArrayGroupFirst,
										   &fc_nextidx,
										   (uint32) fc_proc->pgprocno))
			break;
	}

	/*
	 * 如果列表不为空，领导者将清除我们的 XID。没有领导者的跟随者是
	 * 不可能的，因为第一个将自己添加到列表的进程总是将 nextidx 作为
	 * INVALID_PGPROCNO。
	 */
	if (fc_nextidx != INVALID_PGPROCNO)
	{
		int			fc_extraWaits = 0;

		/* 睡眠直到领导者清除我们的 XID。 */
		pgstat_report_wait_start(WAIT_EVENT_PROCARRAY_GROUP_UPDATE);
		for (;;)
		{
			/* 充当读取屏障 */
			PGSemaphoreLock(fc_proc->sem);
			if (!fc_proc->procArrayGroupMember)
				break;
			fc_extraWaits++;
		}
		pgstat_report_wait_end();

		Assert(pg_atomic_read_u32(&fc_proc->procArrayGroupNext) == INVALID_PGPROCNO);

		/* 修复被吸收的唤醒的信号量计数 */
		while (fc_extraWaits-- > 0)
			PGSemaphoreUnlock(fc_proc->sem);
		return;
	}

	/* 我们是领导者。 代表所有人获取锁。 */
	LWLockAcquire(ProcArrayLock, LW_EXCLUSIVE);

	/*
	 * 现在我们获得了锁，清除等待组 XID 清除的进程列表，并保存
	 * 列表头的指针。一次一个地弹出元素可能会导致 ABA 问题。
	 */
	fc_nextidx = pg_atomic_exchange_u32(&fc_procglobal->procArrayGroupFirst,
									 INVALID_PGPROCNO);

	/* 记住列表的头，以便在释放锁后进行唤醒。 */
	fc_wakeidx = fc_nextidx;

	/* 遍历列表并清除所有 XID。 */
	while (fc_nextidx != INVALID_PGPROCNO)
	{
		PGPROC	   *fc_nextproc = &allProcs[fc_nextidx];

		fc_ProcArrayEndTransactionInternal(fc_nextproc, fc_nextproc->procArrayGroupMemberXid);

		/* 移动到列表中的下一个 proc。 */
		fc_nextidx = pg_atomic_read_u32(&fc_nextproc->procArrayGroupNext);
	}

	/* 我们现在已经完成了锁。 */
	LWLockRelease(ProcArrayLock);

	/*
	 * 现在我们释放了锁，回去唤醒所有人。我们在锁下不执行此操作，以便
	 * 将锁保持时间最小化。我们需要执行的唤醒其他进程的系统调用
	 * 可能比我们在持有锁时所做的简单内存写入要慢得多。
	 */
	while (fc_wakeidx != INVALID_PGPROCNO)
	{
		PGPROC	   *fc_nextproc = &allProcs[fc_wakeidx];

		fc_wakeidx = pg_atomic_read_u32(&fc_nextproc->procArrayGroupNext);
		pg_atomic_write_u32(&fc_nextproc->procArrayGroupNext, INVALID_PGPROCNO);

		/* 确保所有先前的写入在跟随者继续之前都是可见的。 */
		pg_write_barrier();

		fc_nextproc->procArrayGroupMember = false;

		if (fc_nextproc != MyProc)
			PGSemaphoreUnlock(fc_nextproc->sem);
	}
}

/*
 * ProcArrayClearTransaction -- 清除事务字段
 *
 * 这在成功准备 2 阶段事务后使用。我们实际上并不报告该事务的
 * XID 为不再运行——它仍然会显现为运行，因为 2PC 的 gxact 也在
 * ProcArray 中。我们只需清除我们自己的 PGPROC。
 */
void ProcArrayClearTransaction(PGPROC *fc_proc)
{
	int			fc_pgxactoff;

	/*
	 * 目前我们需要在这里独占锁定 ProcArrayLock，因为我们
	 * 在下面增加 xactCompletionCount。我们还需要它至少保持共享
	 * 模式，以便 pgproc->pgxactoff 在下面保持不变。
	 *
	 * 然而，由于此操作实际上不会改变任何人对正在运行的
	 * XID 集合的看法（我们的条目与已经插入到 ProcArray
	 * 中的 gxact 是重复的），如果我们将 xactCompletionCount
	 * 设为原子变量，可以将锁级别降低到共享。但是目前看来
	 * 没有必要，因为 2PC 提交已经足够重，加上这不会成为
	 * 瓶颈。如果它真的成为瓶颈，可能还值得考虑将此与后续的
	 * ProcArrayRemove() 合并。
	 */
	LWLockAcquire(ProcArrayLock, LW_EXCLUSIVE);

	fc_pgxactoff = fc_proc->pgxactoff;

	ProcGlobal->xids[fc_pgxactoff] = InvalidTransactionId;
	fc_proc->xid = InvalidTransactionId;

	fc_proc->lxid = InvalidLocalTransactionId;
	fc_proc->xmin = InvalidTransactionId;
	fc_proc->recoveryConflictPending = false;

	Assert(!(fc_proc->statusFlags & PROC_VACUUM_STATE_MASK));
	Assert(!fc_proc->delayChkptFlags);

	/*
	 * 即使事务尚未真正提交，也需要增加完成计数。原因是
	 * GetSnapshotData() 忽略当前事务的 xid，因此如果没有增
	 * 加，我们最终可能会在之后重用快照。这将是一个问题，
	 * 因为这可能不会将准备好的事务视为正在运行。
	 */
	ShmemVariableCache->xactCompletionCount++;

	/* 清除子事务-XID 缓存 */
	Assert(ProcGlobal->subxidStates[fc_pgxactoff].count == fc_proc->subxidStatus.count &&
		   ProcGlobal->subxidStates[fc_pgxactoff].overflowed == fc_proc->subxidStatus.overflowed);
	if (fc_proc->subxidStatus.count > 0 || fc_proc->subxidStatus.overflowed)
	{
		ProcGlobal->subxidStates[fc_pgxactoff].count = 0;
		ProcGlobal->subxidStates[fc_pgxactoff].overflowed = false;
		fc_proc->subxidStatus.count = 0;
		fc_proc->subxidStatus.overflowed = false;
	}

	LWLockRelease(ProcArrayLock);
}

/*
 * 更新 ShmemVariableCache->latestCompletedXid 指向若当前较旧的 latestXid。
 */
static void fc_MaintainLatestCompletedXid(TransactionId fc_latestXid)
{
	FullTransactionId fc_cur_latest = ShmemVariableCache->latestCompletedXid;

	Assert(FullTransactionIdIsValid(fc_cur_latest));
	Assert(!RecoveryInProgress());
	Assert(LWLockHeldByMe(ProcArrayLock));

	if (TransactionIdPrecedes(XidFromFullTransactionId(fc_cur_latest), fc_latestXid))
	{
		ShmemVariableCache->latestCompletedXid =
			fc_FullXidRelativeTo(fc_cur_latest, fc_latestXid);
	}

	Assert(IsBootstrapProcessingMode() ||
		   FullTransactionIdIsNormal(ShmemVariableCache->latestCompletedXid));
}

/*
 * 与 MaintainLatestCompletedXid 相同，但用于 WAL 重放期间。
 */
static void fc_MaintainLatestCompletedXidRecovery(TransactionId fc_latestXid)
{
	FullTransactionId fc_cur_latest = ShmemVariableCache->latestCompletedXid;
	FullTransactionId fc_rel;

	Assert(AmStartupProcess() || !IsUnderPostmaster);
	Assert(LWLockHeldByMe(ProcArrayLock));

	/*
	 * 需要一个 FullTransactionId 来与 latestXid 进行比较。
	 * 不能依赖在恢复中初始化的 latestCompletedXid。但在恢复
	 * 中，安全地访问 nextXid 而无需锁定是可以的。
	 */
	fc_rel = ShmemVariableCache->nextXid;
	Assert(FullTransactionIdIsValid(ShmemVariableCache->nextXid));

	if (!FullTransactionIdIsValid(fc_cur_latest) ||
		TransactionIdPrecedes(XidFromFullTransactionId(fc_cur_latest), fc_latestXid))
	{
		ShmemVariableCache->latestCompletedXid =
			fc_FullXidRelativeTo(fc_rel, fc_latestXid);
	}

	Assert(FullTransactionIdIsNormal(ShmemVariableCache->latestCompletedXid));
}

/*
 * ProcArrayInitRecovery -- 初始化恢复 xid 管理环境
 *
 * 记住启动进程初始化 CLOG 和 subtrans 的位置，以便确保
 * 在恢复时无缝初始化到必要时的点。
 */
void ProcArrayInitRecovery(TransactionId fc_initializedUptoXID)
{
	Assert(standbyState == STANDBY_INITIALIZED);
	Assert(TransactionIdIsNormal(fc_initializedUptoXID));

	/*
	 * 我们将 latestObservedXid 设置为 SUBTRANS 初始化到的 xid，
	 * 以便我们可以从那一点起在 RecordKnownAssignedTransactionIds 中扩展它，
	 * 并在 ProcArrayApplyRecoveryInfo() 时保持一致。
	 */
	latestObservedXid = fc_initializedUptoXID;
	TransactionIdRetreat(latestObservedXid);
}


/*
 * ProcArrayApplyRecoveryInfo -- 应用关于事务ID的恢复信息
 *
 * 我们经过三个状态：初始化、待处理和就绪。
 * 正常情况下，直接到达就绪状态，但在某些非典型情况下，我们需要逐步进行。
 *
 * 使用有关主节点上运行的事务的数据来创建 KnownAssignedXids 的初始状态。
 * 我们还使用这些记录定期修剪 KnownAssignedXids，因为我们知道一些
 * 发生致命错误的事务可能未能写入中止记录，这可能导致最终溢出。
 *
 * 请参见 LogStandbySnapshot() 的注释。
 */
void ProcArrayApplyRecoveryInfo(RunningTransactions fc_running)
{
	TransactionId *fc_xids;
	int			fc_nxids;
	int			fc_i;

	Assert(standbyState >= STANDBY_INITIALIZED);
	Assert(TransactionIdIsValid(fc_running->nextXid));
	Assert(TransactionIdIsValid(fc_running->oldestRunningXid));
	Assert(TransactionIdIsNormal(fc_running->latestCompletedXid));

	/*
	 * 如果有过期的事务，移除它们。
	 */
	ExpireOldKnownAssignedTransactionIds(fc_running->oldestRunningXid);

	/*
	 * 如果有过期的锁，移除它们。
	 */
	StandbyReleaseOldLocks(fc_running->oldestRunningXid);

	/*
	 * 如果我们的快照已经有效，则无需其他操作...
	 */
	if (standbyState == STANDBY_SNAPSHOT_READY)
		return;

	/*
	 * 如果我们初始的 RunningTransactionsData 有一个溢出的快照，那么
	 * 我们知道我们的快照中缺少一些 subxids。如果我们继续看到溢出的快照，
	 * 那么我们可能永远无法启动，因此我们需要进行另一个测试以查看我们的
	 * 快照是否有效。我们知道缺失的 subxids 小于或早于 nextXid。在初始化后，
	 * 我们在恢复期间继续应用更改，因此一旦 oldestRunningXid 晚于
	 * 初始快照中的 nextXid，我们就知道不再缺少信息，并可以将快照标记为有效。
	 */
	if (standbyState == STANDBY_SNAPSHOT_PENDING)
	{
		/*
		 * 如果快照没有溢出或为空，我们可以重置待处理状态并使用这个快照。
		 */
		if (fc_running->subxid_status != SUBXIDS_MISSING || fc_running->xcnt == 0)
		{
			/*
			 * 如果我们已经收集了已知分配的事务ID，则需要在应用恢复快照之前将它们丢弃。
			 */
			fc_KnownAssignedXidsReset();
			standbyState = STANDBY_INITIALIZED;
		}
		else
		{
			if (TransactionIdPrecedes(standbySnapshotPendingXmin,
									  fc_running->oldestRunningXid))
			{
				standbyState = STANDBY_SNAPSHOT_READY;
				elog(trace_recovery(DEBUG1),
					 "recovery snapshots are now enabled");
			}
			else
				elog(trace_recovery(DEBUG1),
					 "recovery snapshot waiting for non-overflowed snapshot or "
					 "until oldest active xid on standby is at least %u (now %u)",
					 standbySnapshotPendingXmin,
					 fc_running->oldestRunningXid);
			return;
		}
	}

	Assert(standbyState == STANDBY_INITIALIZED);

	/*
	 * 请注意：这至少可以被访问两次，因此确保新代码可以处理这种情况。
	 */

	/*
	 * 还没有其他人在运行，但无论如何都要获取锁
	 */
	LWLockAcquire(ProcArrayLock, LW_EXCLUSIVE);

	/*
	 * KnownAssignedXids 已经排序，因此我们不能直接添加 xids，必须先
	 * 对它们进行排序。
	 *
	 * 一些新的 xids 是顶级 xids，一些是子事务。
	 * 我们不调用 SubTransSetParent，因为这还不重要。如果没有溢出，
	 * 则所有 xids 都将适合快照，因此我们不需要子事务。如果之后出现
	 * 溢出，xid 分配记录将把 xids 添加到子事务中。如果 RunningTransactionsData
	 * 出现溢出，那么我们无论如何都没有足够的信息来正确更新子事务。
	 */

	/*
	 * 分配一个临时数组以避免修改作为参数传入的数组。
	 */
	fc_xids = palloc(sizeof(TransactionId) * (fc_running->xcnt + fc_running->subxcnt));

	/*
	 * 将任何尚未完成的 xids 添加到临时数组中。
	 */
	fc_nxids = 0;
	for (fc_i = 0; fc_i < fc_running->xcnt + fc_running->subxcnt; fc_i++)
	{
		TransactionId fc_xid = fc_running->xids[fc_i];

		/*
		 * 运行事务快照可以包含在快照被提取时仍然在 procarray 中可见
		 * 的 xids，但它们已经被 WAL 记录为完成。它们不再运行，因此忽
		 * 略它们。
		 */
		if (TransactionIdDidCommit(fc_xid) || TransactionIdDidAbort(fc_xid))
			continue;

		fc_xids[fc_nxids++] = fc_xid;
	}

	if (fc_nxids > 0)
	{
		if (procArray->numKnownAssignedXids != 0)
		{
			LWLockRelease(ProcArrayLock);
			elog(ERROR, "KnownAssignedXids is not empty");
		}

		/*
		 * 对数组进行排序，以便我们可以安全地将它们添加到
		 * KnownAssignedXids 中。
		 *
		 * 我们必须逻辑上对它们进行排序，因为在 KnownAssignedXidsAdd 中
		 * 我们调用了 TransactionIdFollowsOrEquals 等等。但是我们知道这些 XIDs
		 * 来自 RUNNING_XACTS，这意味着它们都是来自同一时期的普通 XIDs，
		 * 因此这是安全的。
		 */
		qsort(fc_xids, fc_nxids, sizeof(TransactionId), xidLogicalComparator);

		/*
		 * 将排序后的快照添加到 KnownAssignedXids 中。运行事务快照
		 * 可能因准备好的事务而包含重复的 xids，因此忽略它们。
		 */
		for (fc_i = 0; fc_i < fc_nxids; fc_i++)
		{
			if (fc_i > 0 && TransactionIdEquals(fc_xids[fc_i - 1], fc_xids[fc_i]))
			{
				elog(DEBUG1,
					 "found duplicated transaction %u for KnownAssignedXids insertion",
					 fc_xids[fc_i]);
				continue;
			}
			fc_KnownAssignedXidsAdd(fc_xids[fc_i], fc_xids[fc_i], true);
		}

		fc_KnownAssignedXidsDisplay(trace_recovery(DEBUG3));
	}

	pfree(fc_xids);

	/*
	 * latestObservedXid 至少设置为 SUBTRANS 启动时的点（参见
	 * ProcArrayInitRecovery()），或设置为调用
	 * RecordKnownAssignedTransactionIds() 的最大 xid。从此初始化
	 * 子事务，直到 nextXid - 1。
	 *
	 * 我们需要在这里复制 RecordKnownAssignedTransactionId() 的部分内容，
	 * 因为我们刚刚将 xids 添加到尚未通过 RecordKnownAssignedTransactionId()
	 * 处理的已知分配 xids 机制中。
	 */
	Assert(TransactionIdIsNormal(latestObservedXid));
	TransactionIdAdvance(latestObservedXid);
	while (TransactionIdPrecedes(latestObservedXid, fc_running->nextXid))
	{
		ExtendSUBTRANS(latestObservedXid);
		TransactionIdAdvance(latestObservedXid);
	}
	TransactionIdRetreat(latestObservedXid);	/* = running->nextXid - 1 */

	/* ----------
	 * 现在我们得到了运行的 xids，我们需要设置用于跟踪快照随其
	 * 进一步演变的全局值。
	 *
	 * - latestCompletedXid 将是快照的 xmax
	 * - lastOverflowedXid 显示快照是否溢出
	 * - nextXid
	 *
	 * 如果快照溢出，则我们仍然以我们所知道的进行初始化，
	 * 但恢复快照尚未完全有效，因为我们知道缺少一些 subxids。
	 * 我们不知道缺少的是哪些特定的 subxids，因此保守地假设
	 * 最后一个是 latestObservedXid。
	 * ----------
	 */
	if (fc_running->subxid_status == SUBXIDS_MISSING)
	{
		standbyState = STANDBY_SNAPSHOT_PENDING;

		standbySnapshotPendingXmin = latestObservedXid;
		procArray->lastOverflowedXid = latestObservedXid;
	}
	else
	{
		standbyState = STANDBY_SNAPSHOT_READY;

		standbySnapshotPendingXmin = InvalidTransactionId;

		/*
		 * 如果 'xids' 数组没有包含所有子事务，我们必须将任何
		 * 拍摄的快照标记为溢出。
		 */
		if (fc_running->subxid_status == SUBXIDS_IN_SUBTRANS)
			procArray->lastOverflowedXid = latestObservedXid;
		else
		{
			Assert(fc_running->subxid_status == SUBXIDS_IN_ARRAY);
			procArray->lastOverflowedXid = InvalidTransactionId;
		}
	}

	/*
	 * 如果一个事务在拍摄和记录快照之间写入了提交记录，
	 * 则 latestCompletedXid 可能已经高于快照中的值，
	 * 因此在使用传入值之前要进行检查。它也可能尚未设置。
	 */
	fc_MaintainLatestCompletedXidRecovery(fc_running->latestCompletedXid);

	/*
	 * 注意：这里无需递增 ShmemVariableCache->xactCompletionCount，
	 * 没有人可以看到它。
	 */

	LWLockRelease(ProcArrayLock);

	/* ShmemVariableCache->nextXid 必须在任何观察到的 xid 之上。 */
	AdvanceNextFullTransactionIdPastXid(latestObservedXid);

	Assert(FullTransactionIdIsValid(ShmemVariableCache->nextXid));

	fc_KnownAssignedXidsDisplay(trace_recovery(DEBUG3));
	if (standbyState == STANDBY_SNAPSHOT_READY)
		elog(trace_recovery(DEBUG1), "recovery snapshots are now enabled");
	else
		elog(trace_recovery(DEBUG1),
			 "recovery snapshot waiting for non-overflowed snapshot or "
			 "until oldest active xid on standby is at least %u (now %u)",
			 standbySnapshotPendingXmin,
			 fc_running->oldestRunningXid);
}

/*
 * ProcArrayApplyXidAssignment
 *		处理一个 XLOG_XACT_ASSIGNMENT WAL 记录
 */
void ProcArrayApplyXidAssignment(TransactionId fc_topxid,
							int fc_nsubxids, TransactionId *fc_subxids)
{
	TransactionId fc_max_xid;
	int			fc_i;

	Assert(standbyState >= STANDBY_INITIALIZED);

	fc_max_xid = TransactionIdLatest(fc_topxid, fc_nsubxids, fc_subxids);

	/*
	 * 将所有子事务标记为已观察。
	 *
	 * 注意：如果 subxid 包含太多之前未观察到的 xids，
	 * 以至于无法适应已知分配的 xids，操作将失败。根据当前代码，
	 * 这种情况不应发生，因为 xid 分配记录不应包含
	 * 超过 PGPROC_MAX_CACHED_SUBXIDS 的条目。
	 */
	RecordKnownAssignedTransactionIds(fc_max_xid);

	/*
	 * 请注意，我们使用顶级 xid 更新 pg_subtrans，而不是
	 * 父 xid。这是正常处理和恢复之间的区别，但在所有情况下都是正确的。
	 * 原因是，子事务的提交在提交处理中之前不会在 clog 中标记，
	 * 因此所有中止的子事务已经在 clog 中明确标记。
	 * 因此，我们能够直接引用顶级事务的状态，而不是跳过
	 * 子事务树中的所有中间状态。这应该是我们首次尝试
	 * SubTransSetParent()。
	 */
	for (fc_i = 0; fc_i < fc_nsubxids; fc_i++)
		SubTransSetParent(fc_subxids[fc_i], fc_topxid);

	/* 目前尚未维护 KnownAssignedXids，因此我们完成了。 */
	if (standbyState == STANDBY_INITIALIZED)
		return;

	/*
	 * 使用与事务提交相同的锁定
	 */
	LWLockAcquire(ProcArrayLock, LW_EXCLUSIVE);

	/*
	 * 从已知分配的事务中移除 subxids。
	 */
	fc_KnownAssignedXidsRemoveTree(InvalidTransactionId, fc_nsubxids, fc_subxids);

	/*
	 * 将 lastOverflowedXid 提升到至少为这些 subxids 的最后一个。
	 */
	if (TransactionIdPrecedes(procArray->lastOverflowedXid, fc_max_xid))
		procArray->lastOverflowedXid = fc_max_xid;

	LWLockRelease(ProcArrayLock);
}


/*
 * TransactionIdIsInProgress -- 给定的事务是否在某个后端运行
 *
 * 除了一些快捷方式，例如检查 RecentXmin 和我们自己的 Xid， 
 * 找到正在运行的事务有四种可能性：
 *
 * 1. 给定的 Xid 是一个主事务 ID。我们可以通过查看 ProcGlobal->xids 
 * 轻松得知这一点。
 *
 * 2. 给定的 Xid 是 PGPROC 数组中缓存的子事务 Xid 之一。 
 * 我们也可以轻松找到这一点。
 *
 * 3. 在热备用模式下，我们必须搜索 KnownAssignedXids 列表 
 * 以查看 Xid 是否在主节点上运行。
 *
 * 4. 搜索 SubTrans 树以找到 Xid 的最上层父节点，然后查看 
 * 根据 ProcGlobal->xids[] 或 KnownAssignedXids 是否正在运行。 
 * 这是最慢的方式，但可悲的是，如果其他方法失败，则必须始终这样做， 
 * 除非我们看到缓存的子事务集合是完整的（没有溢出）。
 *
 * 在执行 1、2、3 时必须保持 ProcArrayLock 锁。 
 * 如果在执行 1 和 3 时保存顶部 Xids，我们可以在执行 4 时释放 
 * ProcArrayLock 锁。这会恢复一些并发性（而且我们无论如何不能 
 * 再从 ProcGlobal->xids[] 中检索主 Xids；参见 GetNewTransactionId）。
 */
bool TransactionIdIsInProgress(TransactionId fc_xid)
{
	static TransactionId *fc_xids = NULL;
	static TransactionId *fc_other_xids;
	XidCacheStatus *fc_other_subxidstates;
	int			fc_nxids = 0;
	ProcArrayStruct *fc_arrayP = procArray;
	TransactionId fc_topxid;
	TransactionId fc_latestCompletedXid;
	int			fc_mypgxactoff;
	int			fc_numProcs;
	int			fc_j;

	/*
	 * 不用检查比 RecentXmin 更早的事务；它不可能仍在运行。 
	 * （注：特别地，这保证我们拒绝 InvalidTransactionId、 
	 * FrozenTransactionId 等被认为是不再运行的。）
	 */
	if (TransactionIdPrecedes(fc_xid, RecentXmin))
	{
		xc_by_recent_xmin_inc();
		return false;
	}

	/*
	 * 我们可能刚刚检查了这个事务的状态，所以如果它已经被 
	 * 确定完成，我们可以在不访问共享内存的情况下退出。
	 */
	if (TransactionIdEquals(cachedXidIsNotInProgress, fc_xid))
	{
		xc_by_known_xact_inc();
		return false;
	}

	/*
	 * 此外，我们可以处理自己的事务（和子事务），而不需 
	 * 访问共享内存。
	 */
	if (TransactionIdIsCurrentTransactionId(fc_xid))
	{
		xc_by_my_xact_inc();
		return true;
	}

	/*
	 * 如果是第一次，则获取工作空间以记住主 XIDs。我们将 
	 * 它永久分配以避免反复的 palloc/pfree 开销。
	 */
	if (fc_xids == NULL)
	{
		/*
		 * 在热备用模式下，保留足够的空间以容纳所有 Xid 
		 * 在已知分配列表中。如果我们稍后完成恢复，则不再需要 
		 * 更大的数组，但我们不打算收缩它。
		 */
		int			fc_maxxids = RecoveryInProgress() ? TOTAL_MAX_CACHED_SUBXIDS : fc_arrayP->maxProcs;

		fc_xids = (TransactionId *) malloc(fc_maxxids * sizeof(TransactionId));
		if (fc_xids == NULL)
			ereport(ERROR,
					(errcode(ERRCODE_OUT_OF_MEMORY),
					 errmsg("out of memory")));
	}

	fc_other_xids = ProcGlobal->xids;
	fc_other_subxidstates = ProcGlobal->subxidStates;

	LWLockAcquire(ProcArrayLock, LW_SHARED);

	/*
	 * 现在我们有了锁，可以检查 latestCompletedXid；如果目标 
	 * Xid 在之后，它肯定仍在运行。
	 */
	fc_latestCompletedXid =
		XidFromFullTransactionId(ShmemVariableCache->latestCompletedXid);
	if (TransactionIdPrecedes(fc_latestCompletedXid, fc_xid))
	{
		LWLockRelease(ProcArrayLock);
		xc_by_latest_xid_inc();
		return true;
	}

	/* 没有捷径，必须遍历数组 */
	fc_mypgxactoff = MyProc->pgxactoff;
	fc_numProcs = fc_arrayP->numProcs;
	for (int fc_pgxactoff = 0; fc_pgxactoff < fc_numProcs; fc_pgxactoff++)
	{
		int			fc_pgprocno;
		PGPROC	   *fc_proc;
		TransactionId fc_pxid;
		int			fc_pxids;

		/* 忽略我们自己 --- 上面已经处理过了 */
		if (fc_pgxactoff == fc_mypgxactoff)
			continue;

		/* 仅获取一次 xid - 参见 GetNewTransactionId */
		fc_pxid = UINT32_ACCESS_ONCE(fc_other_xids[fc_pgxactoff]);

		if (!TransactionIdIsValid(fc_pxid))
			continue;

		/*
		 * 第一步：检查主 Xid
		 */
		if (TransactionIdEquals(fc_pxid, fc_xid))
		{
			LWLockRelease(ProcArrayLock);
			xc_by_main_xid_inc();
			return true;
		}

		/*
		 * 我们可以忽略比目标 Xid 年轻的主 Xids，因为
		 * 目标不可能是它们的子级。
		 */
		if (TransactionIdPrecedes(fc_xid, fc_pxid))
			continue;

		/*
		 * 第二步：检查缓存的子 Xids 数组
		 */
		fc_pxids = fc_other_subxidstates[fc_pgxactoff].count;
		pg_read_barrier();		/* 在 GetNewTransactionId() 中与障碍的配对 */
		fc_pgprocno = fc_arrayP->pgprocnos[fc_pgxactoff];
		fc_proc = &allProcs[fc_pgprocno];
		for (fc_j = fc_pxids - 1; fc_j >= 0; fc_j--)
		{
			/* 仅获取一次 xid - 参见 GetNewTransactionId */
			TransactionId fc_cxid = UINT32_ACCESS_ONCE(fc_proc->subxids.xids[fc_j]);

			if (TransactionIdEquals(fc_cxid, fc_xid))
			{
				LWLockRelease(ProcArrayLock);
				xc_by_child_xid_inc();
				return true;
			}
		}

		/*
		 * 保存主 Xid 以便于第四步。我们只需要记住
		 * 有未缓存子级的主 Xids。 （注意：这里没有竞争条件
		 * 因为溢出标志无法被清除，只能设置，在
		 * 我们持有 ProcArrayLock 时。因此我们不会错过一个需要
		 * 关注的 Xid。）
		 */
		if (fc_other_subxidstates[fc_pgxactoff].overflowed)
			fc_xids[fc_nxids++] = fc_pxid;
	}

	/*
	 * 第三步：在热备份模式下，检查已知分配的 xids 列表。XIDs
	 * 在列表中必须被视为正在运行。
	 */
	if (RecoveryInProgress())
	{
		/* 在热备份模式下，所有 PGPROC 条目都不应有 XIDs */
		Assert(fc_nxids == 0);

		if (fc_KnownAssignedXidExists(fc_xid))
		{
			LWLockRelease(ProcArrayLock);
			xc_by_known_assigned_inc();
			return true;
		}

		/*
		 * 如果 KnownAssignedXids 溢出，我们也必须检查 pg_subtrans。
		 * 从 KnownAssignedXids 中提取所有低于
		 * xid 的 xids，因为如果 xid 是子事务，其父级将始终有一个
		 * 更低的值。注意我们将在这里收集主 XIDs 和子 XIDs，
		 * 但没有办法解决这个问题。
		 */
		if (TransactionIdPrecedesOrEquals(fc_xid, procArray->lastOverflowedXid))
			fc_nxids = fc_KnownAssignedXidsGet(fc_xids, fc_xid);
	}

	LWLockRelease(ProcArrayLock);

	/*
	 * 如果没有相关缓存溢出，我们知道该 Xid
	 * 在不查看 pg_subtrans 的情况下并未处于运行状态。
	 */
	if (fc_nxids == 0)
	{
		xc_no_overflow_inc();
		cachedXidIsNotInProgress = fc_xid;
		return false;
	}

	/*
	 * 第四步：必须检查 pg_subtrans。
	 *
	 * 此时，我们知道它要么是 xids[] 中一个 Xid 的子事务，
	 * 要么是未在运行。如果它已经失败的
	 * 子事务，我们想说“未在运行”，即使它的父级可能
	 * 仍在运行。因此，首先检查 pg_xact 以查看它是否已被中止。
	 */
	xc_slow_answer_inc();

	if (TransactionIdDidAbort(fc_xid))
	{
		cachedXidIsNotInProgress = fc_xid;
		return false;
	}

	/*
	 * 它没有被中止，所以检查它所属的事务树
	 * 是否仍在运行（更准确地说，它在我们
	 * 持有 ProcArrayLock 时是否还在运行）。
	 */
	fc_topxid = SubTransGetTopmostTransaction(fc_xid);
	Assert(TransactionIdIsValid(fc_topxid));
	if (!TransactionIdEquals(fc_topxid, fc_xid))
	{
		for (int fc_i = 0; fc_i < fc_nxids; fc_i++)
		{
			if (TransactionIdEquals(fc_xids[fc_i], fc_topxid))
				return true;
		}
	}

	cachedXidIsNotInProgress = fc_xid;
	return false;
}

/*
 * TransactionIdIsActive -- xid 是否是活跃后端的顶级 XID？
 *
 * 这与 TransactionIdIsInProgress 不同，因为它忽略预处理
 * 事务，以及如果我们处于
 * 热备份状态时在主服务器上运行的事务。此外，我们忽略子事务，因为当前
 * 用途不需要。
 */
bool TransactionIdIsActive(TransactionId fc_xid)
{
	bool		fc_result = false;
	ProcArrayStruct *fc_arrayP = procArray;
	TransactionId *fc_other_xids = ProcGlobal->xids;
	int			fc_i;

	/*
	 * 不要费心检查一个早于 RecentXmin 的事务；它不可能
	 * 仍然在运行。
	 */
	if (TransactionIdPrecedes(fc_xid, RecentXmin))
		return false;

	LWLockAcquire(ProcArrayLock, LW_SHARED);

	for (fc_i = 0; fc_i < fc_arrayP->numProcs; fc_i++)
	{
		int			fc_pgprocno = fc_arrayP->pgprocnos[fc_i];
		PGPROC	   *fc_proc = &allProcs[fc_pgprocno];
		TransactionId fc_pxid;

		/* 仅获取一次 xid - 参见 GetNewTransactionId */
		fc_pxid = UINT32_ACCESS_ONCE(fc_other_xids[fc_i]);

		if (!TransactionIdIsValid(fc_pxid))
			continue;

		if (fc_proc->pid == 0)
			continue;			/* 忽略预处理事务 */

		if (TransactionIdEquals(fc_pxid, fc_xid))
		{
			fc_result = true;
			break;
		}
	}

	LWLockRelease(ProcArrayLock);

	return fc_result;
}



/*
 * 确定XID视界。
 *
 * 这由像GetOldestNonRemovableTransactionId()（用于VACUUM）、GetReplicationHorizons()（用于hot_standby_feedback）等包装函数使用，以及GlobalVisUpdate()（见结构体GlobalVisState上面的注释）“内部”使用。
 *
 * 有关各种计算视界的定义，请参见ComputeXidHorizonsResult。
 *
 * 对于VACUUM，单独计算视界（用于决定哪些删除的元组必须被保留），对于共享和非共享表进行计算。对于共享关系，必须考虑所有数据库中的后端，但对于非共享关系则不需要，因为只有我自己数据库中的后端能看到这些元组。此外，我们可以忽略同时运行的惰性VACUUM，因为（a）它们必须在其他表上工作，（b）它们不需要进行基于快照的查找。
 *
 * 这还计算用于截断pg_subtrans的视界。对于此，必须考虑所有数据库中的后端，同时运行的惰性VACUUM不能被忽略，因为它们仍可能执行pg_subtrans访问。
 *
 * 注意：我们在考虑的xids集合中包含所有当前正在运行的xids。这确保如果刚开始的事务尚未设置其快照，当它设置快照时，它无法设置xmin小于我们计算的值。请参见src/backend/access/transam/README中的注释。
 *
 * 注意：尽管如此，重复调用时计算的值可能会向后移动。计算的值是保守的，因此任何更旧的内容肯定不会被任何人再视为正在运行，但计算的确切值取决于许多因素。例如，如果当前数据库中没有事务在运行，普通表的视界将是latestCompletedXid。如果在此之后开始一个事务，其xmin将包括在其他数据库中较早开始的进行中事务，因此另一次调用将返回一个较低的值。尽管如此，用第一次结果对当前数据库中的表进行VACUUM是安全的。也有与复制相关的影响：一个walsender进程可以基于不再在主数据库中运行但仍在备用数据库中回放的事务设置其xmin，从而可能导致值向后移动。在这种情况下，可能会丢失备用希望拥有的数据，但除非备用使用复制插槽使其xmin持久化，否则我们对此无能为力——仅当walsender在备用上执行查询时持续运行，数据才会受到保护。（热备用代码通过使需要访问已移除数据的备用查询失败来处理此类情况，因此没有完整性错误。）此外，计算的值也会受到vacuum_defer_cleanup_age的调整，因此动态增加该设置是另一个轻松的方法，可以使视界向后移动，而不会对数据完整性产生后果。
 *
 * 注意：近似视界（见GlobalVisState的定义）通过这里的计算进行更新。目前这是确保正确性和进行小优化所必需的。如果不这样做，堆VACUUM对heap_page_prune()的调用可能会使用比稍后决定可以删除的元组时更保守的视界——代码对此并不期待（打破HOT）。
 */
static void fc_ComputeXidHorizons(ComputeXidHorizonsResult *fc_h)
{
	ProcArrayStruct *fc_arrayP = procArray;
	TransactionId fc_kaxmin;
	bool		fc_in_recovery = RecoveryInProgress();
	TransactionId *fc_other_xids = ProcGlobal->xids;

	/* 在释放ProcArrayLock后推断 */
	fc_h->catalog_oldest_nonremovable = InvalidTransactionId;

	LWLockAcquire(ProcArrayLock, LW_SHARED);

	fc_h->latest_completed = ShmemVariableCache->latestCompletedXid;

	/*
	 * 我们将MIN()计算初始化为latestCompletedXid + 1。这是可能在ProcArray中稍后出现的XIDs的下限，因此可以防止因未来的添加而高估结果。
	 */
	{
		TransactionId fc_initial;

		fc_initial = XidFromFullTransactionId(fc_h->latest_completed);
		Assert(TransactionIdIsValid(fc_initial));
		TransactionIdAdvance(fc_initial);

		fc_h->oldest_considered_running = fc_initial;
		fc_h->shared_oldest_nonremovable = fc_initial;
		fc_h->data_oldest_nonremovable = fc_initial;

		/*
		 * 只有此后端所做的修改影响临时关系的视界。与其在遍历所有PGPROCs的每次迭代中进行检查，不如简单地将任何顶级xid初始化为当前值。
		 *
		 * 如果没有分配的xid，我们可以使用尽可能激进的视界ReadNewTransactionid()，但我们可以使用更便宜的latestCompletedXid + 1：如果此后端没有xid，根据定义，临时表中不可能有比latestCompletedXid更新的更改。
		 */
		if (TransactionIdIsValid(MyProc->xid))
			fc_h->temp_oldest_nonremovable = MyProc->xid;
		else
			fc_h->temp_oldest_nonremovable = fc_initial;
	}

	/*
	 * 当ProcArrayLock被持有时获取槽视界——LWLockAcquire/LWLockRelease是一个屏障，确保在锁内部发生此操作。
	 */
	fc_h->slot_xmin = procArray->replication_slot_xmin;
	fc_h->slot_catalog_xmin = procArray->replication_slot_catalog_xmin;

	for (int fc_index = 0; fc_index < fc_arrayP->numProcs; fc_index++)
	{
		int			fc_pgprocno = fc_arrayP->pgprocnos[fc_index];
		PGPROC	   *fc_proc = &allProcs[fc_pgprocno];
		int8		fc_statusFlags = ProcGlobal->statusFlags[fc_index];
		TransactionId fc_xid;
		TransactionId fc_xmin;

		/* 仅获取一次 xid - 参见 GetNewTransactionId */
		fc_xid = UINT32_ACCESS_ONCE(fc_other_xids[fc_index]);
		fc_xmin = UINT32_ACCESS_ONCE(fc_proc->xmin);

		/*
		 * 考虑事务的Xmin和Xid。
		 *
		 * 我们必须检查这两个，因为一个事务可能有Xmin但没有（尚未）Xid；相反，如果它有Xid，这可能决定一些尚未设置的Xmin。
		 */
		fc_xmin = TransactionIdOlder(fc_xmin, fc_xid);

		/* 如果都没有设置，则此进程不影响视界 */
		if (!TransactionIdIsValid(fc_xmin))
			continue;

		
/*
		 * 在确定哪些事务可能被视为正在运行时，不能忽略任何进程。
		 * 虽然插槽应确保逻辑解码后端即使在没有此检查的情况下也受到保护，但将它们包含在这里并没有坏处。
		 */
		fc_h->oldest_considered_running =
			TransactionIdOlder(fc_h->oldest_considered_running, fc_xmin);

		/*
		 * 跳过正在进行清理（只要 pg_subtrans 没有被截断，这样是可以接受的）或进行逻辑解码的后端（其 xmin 单独管理，见下文）。
		 */
		if (fc_statusFlags & (PROC_IN_VACUUM | PROC_IN_LOGICAL_DECODING))
			continue;

		/* 共享表需要考虑所有数据库中的后端 */
		fc_h->shared_oldest_nonremovable =
			TransactionIdOlder(fc_h->shared_oldest_nonremovable, fc_xmin);

		/*
		 * 通常，其他数据库中的会话在共享范围之外是被忽略的。
		 *
		 * 然而，当 MyDatabaseId 尚未设置时，应该包含它们。
		 * 正在启动的后端不得计算“过于激进”的范围，否则我们可能会使用它来修剪仍然需要的数据。
		 * 如果当前后端从未连接到数据库，这没有害处，因为 data_oldest_nonremovable 将永远不会被利用。
		 *
		 * 此外，标记为 PROC_AFFECTS_ALL_HORIZONS 的会话应始终被包含。（该标志用于热备份反馈，不能与特定数据库绑定。）
		 *
		 * 此外，在恢复过程中我们无法计算每个数据库的准确范围，因为所有 xid 都是通过 KnownAssignedXids 机制管理的。
		 */
		if (fc_proc->databaseId == MyDatabaseId ||
			MyDatabaseId == InvalidOid ||
			(fc_statusFlags & PROC_AFFECTS_ALL_HORIZONS) ||
			fc_in_recovery)
		{
			fc_h->data_oldest_nonremovable =
				TransactionIdOlder(fc_h->data_oldest_nonremovable, fc_xmin);
		}
	}

	/*
	 * 如果在恢复过程中，获取 KnownAssignedXids 中最旧的 xid，在释放锁后将应用。
	 */
	if (fc_in_recovery)
		fc_kaxmin = fc_KnownAssignedXidsGetOldestXmin();

	/*
	 * 不需要来自共享状态的其他信息，立即释放锁。其余计算可以在没有锁的情况下完成。
	 */
	LWLockRelease(ProcArrayLock);

	if (fc_in_recovery)
	{
		fc_h->oldest_considered_running =
			TransactionIdOlder(fc_h->oldest_considered_running, fc_kaxmin);
		fc_h->shared_oldest_nonremovable =
			TransactionIdOlder(fc_h->shared_oldest_nonremovable, fc_kaxmin);
		fc_h->data_oldest_nonremovable =
			TransactionIdOlder(fc_h->data_oldest_nonremovable, fc_kaxmin);
		/* 临时关系在恢复过程中无法访问 */
	}
	else
	{
		/*
		 * 通过减去 vacuum_defer_cleanup_age 计算截止 XID。
		 *
		 * vacuum_defer_cleanup_age 为热备份服务器上的查询提供了一些额外的“松弛”。
		 * 这既快速又简单，也许在主服务器的事务速率可预测的情况下不那么有用，但它在没有 walsender 连接时提供了一些保护。
		 * 注意，我们假设 vacuum_defer_cleanup_age 不会大到造成溢出——因此 guc.c 应该将其限制在 varsup.c 中的 xidStopLimit 阈值之内。
		 * 还要注意，我们故意不在备份服务器上应用 vacuum_defer_cleanup_age。
		 *
		 * 需要使用 TransactionIdRetreatSafely()，而不是使用开代码进行减法，以防止在 FirstNormalTransactionId 之前创建 xid。
		 */
		Assert(TransactionIdPrecedesOrEquals(fc_h->oldest_considered_running,
											 fc_h->shared_oldest_nonremovable));
		Assert(TransactionIdPrecedesOrEquals(fc_h->shared_oldest_nonremovable,
											 fc_h->data_oldest_nonremovable));

		if (vacuum_defer_cleanup_age > 0)
		{
			fc_TransactionIdRetreatSafely(&fc_h->oldest_considered_running,
									   vacuum_defer_cleanup_age,
									   fc_h->latest_completed);
			fc_TransactionIdRetreatSafely(&fc_h->shared_oldest_nonremovable,
									   vacuum_defer_cleanup_age,
									   fc_h->latest_completed);
			fc_TransactionIdRetreatSafely(&fc_h->data_oldest_nonremovable,
									   vacuum_defer_cleanup_age,
									   fc_h->latest_completed);
			/* defer 不适用于临时关系 */


			Assert(TransactionIdPrecedesOrEquals(fc_h->oldest_considered_running,
												 fc_h->shared_oldest_nonremovable));
			Assert(TransactionIdPrecedesOrEquals(fc_h->shared_oldest_nonremovable,
												 fc_h->data_oldest_nonremovable));
		}
	}

	/*
	 * 检查是否存在需要旧的 xmin 的复制槽。
	 */
	fc_h->shared_oldest_nonremovable =
		TransactionIdOlder(fc_h->shared_oldest_nonremovable, fc_h->slot_xmin);
	fc_h->data_oldest_nonremovable =
		TransactionIdOlder(fc_h->data_oldest_nonremovable, fc_h->slot_xmin);

	/*
	 * 目录 / 数据视野之间的唯一区别是槽的
	 * 目录 xmin 应用于目录（以便可以访问
	 * 逻辑解码）。初始化为数据视野，然后在必要时
	 * 进一步回退。还必须回退共享视野，因为
	 * 它也可能包含目录。
	 */
	fc_h->shared_oldest_nonremovable_raw = fc_h->shared_oldest_nonremovable;
	fc_h->shared_oldest_nonremovable =
		TransactionIdOlder(fc_h->shared_oldest_nonremovable,
						   fc_h->slot_catalog_xmin);
	fc_h->catalog_oldest_nonremovable = fc_h->data_oldest_nonremovable;
	fc_h->catalog_oldest_nonremovable =
		TransactionIdOlder(fc_h->catalog_oldest_nonremovable,
						   fc_h->slot_catalog_xmin);

	/*
	 * 槽 / vacuum_defer_cleanup_age 可能已将
	 * 视野回退得比 oldest_considered_running 更远。修复。
	 */
	fc_h->oldest_considered_running =
		TransactionIdOlder(fc_h->oldest_considered_running,
						   fc_h->shared_oldest_nonremovable);
	fc_h->oldest_considered_running =
		TransactionIdOlder(fc_h->oldest_considered_running,
						   fc_h->catalog_oldest_nonremovable);
	fc_h->oldest_considered_running =
		TransactionIdOlder(fc_h->oldest_considered_running,
						   fc_h->data_oldest_nonremovable);

	/*
	 * 共享视野必须至少与当前数据库中最旧的可见
	 * 内容一样老。
	 */
	Assert(TransactionIdPrecedesOrEquals(fc_h->shared_oldest_nonremovable,
										 fc_h->data_oldest_nonremovable));
	Assert(TransactionIdPrecedesOrEquals(fc_h->shared_oldest_nonremovable,
										 fc_h->catalog_oldest_nonremovable));

	/*
	 * 视野需要确保相关后端仍然可以访问 pg_subtrans。
	 */
	Assert(TransactionIdPrecedesOrEquals(fc_h->oldest_considered_running,
										 fc_h->shared_oldest_nonremovable));
	Assert(TransactionIdPrecedesOrEquals(fc_h->oldest_considered_running,
										 fc_h->catalog_oldest_nonremovable));
	Assert(TransactionIdPrecedesOrEquals(fc_h->oldest_considered_running,
										 fc_h->data_oldest_nonremovable));
	Assert(TransactionIdPrecedesOrEquals(fc_h->oldest_considered_running,
										 fc_h->temp_oldest_nonremovable));
	Assert(!TransactionIdIsValid(fc_h->slot_xmin) ||
		   TransactionIdPrecedesOrEquals(fc_h->oldest_considered_running,
										 fc_h->slot_xmin));
	Assert(!TransactionIdIsValid(fc_h->slot_catalog_xmin) ||
		   TransactionIdPrecedesOrEquals(fc_h->oldest_considered_running,
										 fc_h->slot_catalog_xmin));

	/* 使用计算出的视野更新近似视野 */
	fc_GlobalVisUpdateApply(fc_h);
}

/*
 * 确定需要为关系使用哪种可见性视野。如果 rel 为 NULL，则
 * 使用最保守的视野。
 */
static inline GlobalVisHorizonKind
fc_GlobalVisHorizonKindForRel(Relation fc_rel)
{
	/*
	 * 其他 relkkinds 目前不包含 xids，也并不总是包含
	 * 必要的逻辑解码标记。
	 */
	Assert(!fc_rel ||
		   fc_rel->rd_rel->relkind == RELKIND_RELATION ||
		   fc_rel->rd_rel->relkind == RELKIND_MATVIEW ||
		   fc_rel->rd_rel->relkind == RELKIND_TOASTVALUE);

	if (fc_rel == NULL || fc_rel->rd_rel->relisshared || RecoveryInProgress())
		return VISHORIZON_SHARED;
	else if (IsCatalogRelation(fc_rel) ||
			 RelationIsAccessibleInLogicalDecoding(fc_rel))
		return VISHORIZON_CATALOG;
	else if (!RELATION_IS_LOCAL(fc_rel))
		return VISHORIZON_DATA;
	else
		return VISHORIZON_TEMP;
}

/*
 * 返回必须保留的已删除元组的最旧 XID
 * 在传入的表中。
 *
 * 如果 rel 不是 NULL，则视野可能比
 * 否则要新得多（即可以移除的元组更少）。在 NULL 情况下，
 * 将返回对所有关系都正确（但不最优）的视野。
 *
 * 这由 VACUUM 使用，以决定必须在
 * 传入的表中保留哪些已删除的元组。
 */
TransactionId GetOldestNonRemovableTransactionId(Relation fc_rel)
{
	ComputeXidHorizonsResult fc_horizons;

	fc_ComputeXidHorizons(&fc_horizons);

	switch (fc_GlobalVisHorizonKindForRel(fc_rel))
	{
		case VISHORIZON_SHARED:
			return fc_horizons.shared_oldest_nonremovable;
		case VISHORIZON_CATALOG:
			return fc_horizons.catalog_oldest_nonremovable;
		case VISHORIZON_DATA:
			return fc_horizons.data_oldest_nonremovable;
		case VISHORIZON_TEMP:
			return fc_horizons.temp_oldest_nonremovable;
	}

	/* 仅用于防止编译器警告 */
	return InvalidTransactionId;
}

/*
 * 返回任何当前运行的后端可能仍然考虑运行的
 * 最旧事务 ID。这不应该用于可见性 / 修剪
 * 决定（见 GetOldestNonRemovableTransactionId()），而是用于
 * 决策，比如 pg_subtrans 可以截断到哪里。
 */
TransactionId GetOldestTransactionIdConsideredRunning(void)
{
	ComputeXidHorizonsResult fc_horizons;

	fc_ComputeXidHorizons(&fc_horizons);

	return fc_horizons.oldest_considered_running;
}

/*
 * 返回热备用反馈消息的可见性视野。
 */
void GetReplicationHorizons(TransactionId *fc_xmin, TransactionId *fc_catalog_xmin)
{
	ComputeXidHorizonsResult fc_horizons;

	fc_ComputeXidHorizons(&fc_horizons);

	/*
	 * 不想在这里使用 shared_oldest_nonremovable，因为那包含
	 * 复制槽的 catalog_xmin 的效果。我们想发送一个单独的
	 * 针对目录视野的反馈，以便主节点可以更积极地
	 * 移除数据表内容。
	 */
	*fc_xmin = fc_horizons.shared_oldest_nonremovable_raw;
	*fc_catalog_xmin = fc_horizons.slot_catalog_xmin;
}

/*
 * GetMaxSnapshotXidCount -- 获取快照 XID 数组的最大大小
 *
 * 我们必须导出这个以供 snapmgr.c 使用。
 */
int GetMaxSnapshotXidCount(void)
{
	return procArray->maxProcs;
}

/*
 * GetMaxSnapshotSubxidCount -- 获取快照子-XID 数组的最大大小
 *
 * 我们必须导出这个以供 snapmgr.c 使用。
 */
int GetMaxSnapshotSubxidCount(void)
{
	return TOTAL_MAX_CACHED_SUBXIDS;
}

/*
 * 初始化新建快照的旧快照阈值特定部分。
 */
static void fc_GetSnapshotDataInitOldSnapshot(Snapshot fc_snapshot)
{
	if (!OldSnapshotThresholdActive())
	{
		/*
		 * 如果不使用“快照过旧”功能，则用
		 * 不需要任何锁定的虚拟值填充相关字段。
		 */
		fc_snapshot->lsn = InvalidXLogRecPtr;
		fc_snapshot->whenTaken = 0;
	}
	else
	{
		/*
		 * 捕获当前时间和 WAL 流位置，以防这个
		 * 快照变得足够陈旧需要回退到特殊的
		 * “旧快照”逻辑。
		 */
		fc_snapshot->lsn = GetXLogInsertRecPtr();
		fc_snapshot->whenTaken = GetSnapshotCurrentTimestamp();
		MaintainOldSnapshotTimeMapping(fc_snapshot->whenTaken, fc_snapshot->xmin);
	}
}

/*
 * GetSnapshotData() 的辅助函数，用于检查快照中的
 * 可见性信息的大部分是否仍然有效。如果是，则更新
 * 需要更改的字段并返回 true。否则返回 false。
 *
 * 这很可能可以演变为不需要持有 ProcArrayLock（至少在
 * 我们已经持有快照的情况下），但那是后话。
 */
static bool fc_GetSnapshotDataReuse(Snapshot fc_snapshot)
{
	uint64		fc_curXactCompletionCount;

	Assert(LWLockHeldByMe(ProcArrayLock));

	if (unlikely(fc_snapshot->snapXactCompletionCount == 0))
		return false;

	fc_curXactCompletionCount = ShmemVariableCache->xactCompletionCount;
	if (fc_curXactCompletionCount != fc_snapshot->snapXactCompletionCount)
		return false;

	/*
	 * 如果当前的 xactCompletionCount 仍与快照构建时相同，
	 * 我们可以确定以硬方式重建快照内容将导致相同的快照
	 * 内容：
	 *
	 * 正如 transam/README 中所解释的，GetSnapshotData() 认为
	 * 正在运行的 xids 集在持有 ProcArrayLock 时不能更改。
	 * 快照内容仅依赖于具有 xids 的事务，并且
	 * 每当具有 xid 的事务完成时（同时独占持有 ProcArrayLock），
	 * xactCompletionCount 就会递增。因此，xactCompletionCount 检查
	 * 确保我们会检测到快照是否会发生变化。
	 *
	 * 由于快照内容与之前相同，因此安全地将
	 * 快照的 xmin 重新输入到 PGPROC 数组中。
	 * 在快照下可见的行没有可能已经被移除（那将
	 * 需要正在运行事务的集合发生变化），并且满足
	 * 并发的 GetSnapshotData() 调用产生相同
	 * xmin 的要求。
	 */
	if (!TransactionIdIsValid(MyProc->xmin))
		MyProc->xmin = TransactionXmin = fc_snapshot->xmin;

	RecentXmin = fc_snapshot->xmin;
	Assert(TransactionIdPrecedesOrEquals(TransactionXmin, RecentXmin));

	fc_snapshot->curcid = GetCurrentCommandId(false);
	fc_snapshot->active_count = 0;
	fc_snapshot->regd_count = 0;
	fc_snapshot->copied = false;

	fc_GetSnapshotDataInitOldSnapshot(fc_snapshot);

	return true;
}


/*
 * GetSnapshotData -- 返回有关正在运行的事务的信息。
 *
 * 返回的快照包括 xmin（最低的仍在运行的事务 ID），
 * xmax（最高完成的事务 ID + 1），以及在 xmin <= xid < xmax 范围内的正在运行的事务 ID 列表。
 * 它的使用如下：
 *        所有 xact ID < xmin 被视为已完成。
 *        所有 xact ID >= xmax 被视为仍在运行。
 *        对于 xact ID xmin <= xid < xmax，查询列表以查看它是否被视为正在运行。
 * 这确保当前事务所看到的“正在运行”的事务集合在它获取快照后不会改变。
 *
 * 所有正在运行的顶级 XIDs 都包含在快照中，除了懒惰的 VACUUM 进程。
 * 我们也尝试包括正在运行的子事务 XIDs，但由于 PGPROC 对于 subxact XIDs 只有有限的缓存区域，
 * 因此可能无法获取完整信息。如果我们发现任何溢出的 subxid 数组，
 * 我们必须将快照的 subxid 数据标记为溢出，并可能需要额外工作
 * *可能*需要完成以确定正在运行的事务（参见 XidInMVCCSnapshot()）。
 *
 * 我们还更新以下后端全局变量：
 *        TransactionXmin：当前事务中使用的任何快照的最旧 xmin（这与 MyProc->xmin 相同）。
 *        RecentXmin：为最新快照计算的 xmin。比这更旧的 XIDs 已知不再运行。
 *
 * 并尝试更新 GlobalVis{Shared,Catalog,Data,Temp}Rels 的边界
 * 以便于 GlobalVisTest* 函数系列的使用。
 *
 * 注意：这个函数可能不应该使用非静态分配的参数进行调用（见下面的 xip 分配）。
 */
Snapshot GetSnapshotData(Snapshot fc_snapshot)
{
	ProcArrayStruct *fc_arrayP = procArray;
	TransactionId *fc_other_xids = ProcGlobal->xids;
	TransactionId fc_xmin;
	TransactionId fc_xmax;
	int			fc_count = 0;
	int			fc_subcount = 0;
	bool		fc_suboverflowed = false;
	FullTransactionId fc_latest_completed;
	TransactionId fc_oldestxid;
	int			fc_mypgxactoff;
	TransactionId fc_myxid;
	uint64		fc_curXactCompletionCount;

	TransactionId fc_replication_slot_xmin = InvalidTransactionId;
	TransactionId fc_replication_slot_catalog_xmin = InvalidTransactionId;

	Assert(fc_snapshot != NULL);

	/*
	 * 为 maxProcs xids 分配空间通常是过多的；numProcs 将足够。
	 * 但在不持有锁的情况下进行 malloc 似乎更好，因此我们无法查看 numProcs。
	 * 同样，我们分配的 subxip 存储远比可能需要的要多。
	 *
	 * 这确实为避免重复的 malloc/free 开辟了可能性：由于
	 * maxProcs 在运行时不会改变，我们可以简单地重用以前的 xip 数组（如果有）。
	 * （这依赖于所有调用者传递静态的 SnapshotData 结构。）
	 */
	if (fc_snapshot->xip == NULL)
	{
		/*
		 * 这是此快照的第一次调用。无论我们是否处于恢复中，快照大小相同，详见后面的评论。
		 */
		fc_snapshot->xip = (TransactionId *)
			malloc(GetMaxSnapshotXidCount() * sizeof(TransactionId));
		if (fc_snapshot->xip == NULL)
			ereport(ERROR,
					(errcode(ERRCODE_OUT_OF_MEMORY),
					 errmsg("out of memory")));
		Assert(fc_snapshot->subxip == NULL);
		fc_snapshot->subxip = (TransactionId *)
			malloc(GetMaxSnapshotSubxidCount() * sizeof(TransactionId));
		if (fc_snapshot->subxip == NULL)
			ereport(ERROR,
					(errcode(ERRCODE_OUT_OF_MEMORY),
					 errmsg("out of memory")));
	}

	/*
	 * 共享锁在 ProcArrayLock 上足够，即使我们要设置 MyProc->xmin。
	 */
	LWLockAcquire(ProcArrayLock, LW_SHARED);

	if (fc_GetSnapshotDataReuse(fc_snapshot))
	{
		LWLockRelease(ProcArrayLock);
		return fc_snapshot;
	}

	fc_latest_completed = ShmemVariableCache->latestCompletedXid;
	fc_mypgxactoff = MyProc->pgxactoff;
	fc_myxid = fc_other_xids[fc_mypgxactoff];
	Assert(fc_myxid == MyProc->xid);

	fc_oldestxid = ShmemVariableCache->oldestXid;
	fc_curXactCompletionCount = ShmemVariableCache->xactCompletionCount;

	/* xmax 始终是 latestCompletedXid + 1 */
	fc_xmax = XidFromFullTransactionId(fc_latest_completed);
	TransactionIdAdvance(fc_xmax);
	Assert(TransactionIdIsNormal(fc_xmax));

	/* 用 xmax 初始化 xmin 计算 */
	fc_xmin = fc_xmax;

	/* 考虑自己的 xid，可以减少循环内的检查 */
	if (TransactionIdIsNormal(fc_myxid) && NormalTransactionIdPrecedes(fc_myxid, fc_xmin))
		fc_xmin = fc_myxid;

	fc_snapshot->takenDuringRecovery = RecoveryInProgress();

	if (!fc_snapshot->takenDuringRecovery)
	{
		int			fc_numProcs = fc_arrayP->numProcs;
		TransactionId *fc_xip = fc_snapshot->xip;
		int		   *fc_pgprocnos = fc_arrayP->pgprocnos;
		XidCacheStatus *fc_subxidStates = ProcGlobal->subxidStates;
		uint8	   *fc_allStatusFlags = ProcGlobal->statusFlags;

		/*
		 * 首先收集需要包含在快照中的 pgxactoff/xids 集合。
		 */
		for (int fc_pgxactoff = 0; fc_pgxactoff < fc_numProcs; fc_pgxactoff++)
		{
			/* 仅获取一次 xid - 参见 GetNewTransactionId */
			TransactionId fc_xid = UINT32_ACCESS_ONCE(fc_other_xids[fc_pgxactoff]);
			uint8		fc_statusFlags;

			Assert(allProcs[fc_arrayP->pgprocnos[fc_pgxactoff]].pgxactoff == fc_pgxactoff);

			/*
			 * 如果事务没有分配 XID，我们可以跳过它；它也不会有子 XIDs。
			 */
			if (likely(fc_xid == InvalidTransactionId))
				continue;

			/*
			 * 我们不将自己的 XID（如果有的话）包含在快照中。它需要包含在 xmin 计算中，但我们已经在循环外完成了这一操作。
			 */
			if (fc_pgxactoff == fc_mypgxactoff)
				continue;

			/*
			 * 我们能够以非正常的 xid 到达这里的唯一方法是在引导时 - 使用 BootstrapTransactionId 的这个后端。但上述测试应该将其过滤掉。
			 */
			Assert(TransactionIdIsNormal(fc_xid));

			/*
			 * 如果 XID >= xmax，我们可以跳过它；这样的事务无论如何将被视为正在运行（任何子 XIDs 也将 >= xmax）。
			 */
			if (!NormalTransactionIdPrecedes(fc_xid, fc_xmax))
				continue;

			/*
			 * 跳过正在单独管理 xmin 的逻辑解码后端（见下文）和正在运行 LAZY VACUUM 的后端。
			 */
			fc_statusFlags = fc_allStatusFlags[fc_pgxactoff];
			if (fc_statusFlags & (PROC_IN_LOGICAL_DECODING | PROC_IN_VACUUM))
				continue;

			if (NormalTransactionIdPrecedes(fc_xid, fc_xmin))
				fc_xmin = fc_xid;

			/* 将 XID 添加到快照中。 */
			fc_xip[fc_count++] = fc_xid;

			/*
			 * 如果可能，保存子事务 XIDs（如果我们已经溢出，则没有意义）。请注意，subxact XIDs 必须晚于其父级，因此不需要与 xmin 检查。我们可以过滤 xmax，但在持有 ProcArrayLock 的时候不进行这么多工作似乎更好。
			 *
			 * 另一个后端可以同时添加更多子 xids，但不能移除任何。因此，仅需一次获取 nxids 便是重要的。虽然使用 memcpy 应该是安全的。（我们不必担心遗漏任何同时添加的 xids，因为它们必须在 xmax 之后。）
			 *
			 * 再次强调，我们自己的 XIDs 不包括在快照中。
			 */
			if (!fc_suboverflowed)
			{

				if (fc_subxidStates[fc_pgxactoff].overflowed)
					fc_suboverflowed = true;
				else
				{
					int			fc_nsubxids = fc_subxidStates[fc_pgxactoff].count;

					if (fc_nsubxids > 0)
					{
						int			fc_pgprocno = fc_pgprocnos[fc_pgxactoff];
						PGPROC	   *fc_proc = &allProcs[fc_pgprocno];

						pg_read_barrier();	/* 与 GetNewTransactionId 配对 */

						memcpy(fc_snapshot->subxip + fc_subcount,
							   (void *) fc_proc->subxids.xids,
							   fc_nsubxids * sizeof(TransactionId));
						fc_subcount += fc_nsubxids;
					}
				}
			}
		}
	}
	else
	{
		/*
		 * 我们处于热备份状态，因此从 KnownAssignedXids 获取 XIDs。
		 *
		 * 我们将所有 xids 直接存储到 subxip[] 中。原因如下：
		 *
		 * 在恢复过程中，我们不知道哪些 xids 是顶层的，哪些是子事务，这一设计选择大大简化了 xid 处理。
		 *
		 * 似乎我们会想尝试仅将 xids 放入 xip[]，但那是相当小的。我们要么需要将其增大，要么提高 WAL 记录 xid 分配的速度；这两者都不是令人愉快的选择。
		 *
		 * 我们可以尝试先将 xids 存储到 xip[] 中，然后如果 xids 太多再存储到 subxip[] 中。只有在快照没有溢出的情况下，这才有效，因为在那种情况下我们不会搜索 subxip[]。更简单的方法是将所有 xids 存储在子事务数组中，因为这是迄今为止更大的数组。我们就把 xip 数组留空。
		 *
		 * 无论是哪种方式，我们需要根据快照的时间改变 XidInMVCCSnapshot() 的工作方式，或者更改正常快照处理以使其匹配。
		 *
		 * 注意：恢复可能在我们完成拍摄快照之前结束，并且新分配的事务 ID 可能被添加到 ProcArray。由于在我们持有 ProcArrayLock 时，xmax 不会改变，因此那些新添加的事务 ID 将被过滤掉，因此我们不必担心它们。
		 */
		fc_subcount = fc_KnownAssignedXidsGetAndSetXmin(fc_snapshot->subxip, &fc_xmin,
												  fc_xmax);

		if (TransactionIdPrecedesOrEquals(fc_xmin, procArray->lastOverflowedXid))
			fc_suboverflowed = true;
	}


	/*
	 * 在持有 ProcArrayLock 的同时提取到局部变量 - 下面的 LWLockRelease 是一个屏障，确保这发生在锁内。
	 */
	fc_replication_slot_xmin = procArray->replication_slot_xmin;
	fc_replication_slot_catalog_xmin = procArray->replication_slot_catalog_xmin;

	if (!TransactionIdIsValid(MyProc->xmin))
		MyProc->xmin = TransactionXmin = fc_xmin;

	LWLockRelease(ProcArrayLock);

	/* 维护 GlobalVis* 的状态 */
	{
		TransactionId fc_def_vis_xid;
		TransactionId fc_def_vis_xid_data;
		FullTransactionId fc_def_vis_fxid;
		FullTransactionId fc_def_vis_fxid_data;
		FullTransactionId fc_oldestfxid;

		/*
		 * 转换 oldestXid 只有在 xid 地平线无法推进时才安全，即保持锁定。虽然我们不再保持锁定，但所有必要的数据都在保持锁定时收集完毕。
		 */
		fc_oldestfxid = fc_FullXidRelativeTo(fc_latest_completed, fc_oldestxid);

		/* 应用 vacuum_defer_cleanup_age */
		fc_def_vis_xid_data = fc_xmin;
		fc_TransactionIdRetreatSafely(&fc_def_vis_xid_data,
								   vacuum_defer_cleanup_age,
								   fc_oldestfxid);

		/* 检查是否有需要更早 xmin 的复制槽。 */
		fc_def_vis_xid_data =
			TransactionIdOlder(fc_def_vis_xid_data, fc_replication_slot_xmin);

		/*
		 * 如果某些行在这 xid 之前，可能可以对非共享、非目录表进行清空。
		 */
		fc_def_vis_xid = fc_def_vis_xid_data;

		/*
		 * 检查是否有需要更早的目录 xmin 的复制槽。
		 */
		fc_def_vis_xid =
			TransactionIdOlder(fc_replication_slot_catalog_xmin, fc_def_vis_xid);

		fc_def_vis_fxid = fc_FullXidRelativeTo(fc_latest_completed, fc_def_vis_xid);
		fc_def_vis_fxid_data = fc_FullXidRelativeTo(fc_latest_completed, fc_def_vis_xid_data);

		/*
		 * 检查我们是否可以增加上限。由于之前的 GlobalVisUpdate() 可能计算了更激进的值，因此如果是这样就不覆盖它们。
		 */
		GlobalVisSharedRels.definitely_needed =
			FullTransactionIdNewer(fc_def_vis_fxid,
								   GlobalVisSharedRels.definitely_needed);
		GlobalVisCatalogRels.definitely_needed =
			FullTransactionIdNewer(fc_def_vis_fxid,
								   GlobalVisCatalogRels.definitely_needed);
		GlobalVisDataRels.definitely_needed =
			FullTransactionIdNewer(fc_def_vis_fxid_data,
								   GlobalVisDataRels.definitely_needed);
		/* 参见 ComputeXidHorizons() 中的 temp_oldest_nonremovable 计算 */
		if (TransactionIdIsNormal(fc_myxid))
			GlobalVisTempRels.definitely_needed =
				fc_FullXidRelativeTo(fc_latest_completed, fc_myxid);
		else
		{
			GlobalVisTempRels.definitely_needed = fc_latest_completed;
			FullTransactionIdAdvance(&GlobalVisTempRels.definitely_needed);
		}

		/*
		 * 检查我们是否知道可以初始化或增加下限。目前唯一便宜的方法是使用 ShmemVariableCache->oldestXid 作为输入。
		 *
		 * 我们肯定能够做得更好。我们可以，例如，将全局下限值放入 ShmemVariableCache。
		 */
		GlobalVisSharedRels.maybe_needed =
			FullTransactionIdNewer(GlobalVisSharedRels.maybe_needed,
								   fc_oldestfxid);
		GlobalVisCatalogRels.maybe_needed =
			FullTransactionIdNewer(GlobalVisCatalogRels.maybe_needed,
								   fc_oldestfxid);
		GlobalVisDataRels.maybe_needed =
			FullTransactionIdNewer(GlobalVisDataRels.maybe_needed,
								   fc_oldestfxid);
		/* 已知准确值 */
		GlobalVisTempRels.maybe_needed = GlobalVisTempRels.definitely_needed;
	}

	RecentXmin = fc_xmin;
	Assert(TransactionIdPrecedesOrEquals(TransactionXmin, RecentXmin));

	fc_snapshot->xmin = fc_xmin;
	fc_snapshot->xmax = fc_xmax;
	fc_snapshot->xcnt = fc_count;
	fc_snapshot->subxcnt = fc_subcount;
	fc_snapshot->suboverflowed = fc_suboverflowed;
	fc_snapshot->snapXactCompletionCount = fc_curXactCompletionCount;

	fc_snapshot->curcid = GetCurrentCommandId(false);

	/*
	 * 这是一个新的快照，因此将两个引用计数设置为零，并标记为未在持久内存中复制。
	 */
	fc_snapshot->active_count = 0;
	fc_snapshot->regd_count = 0;
	fc_snapshot->copied = false;

	fc_GetSnapshotDataInitOldSnapshot(fc_snapshot);

	return fc_snapshot;
}

/*
 * ProcArrayInstallImportedXmin -- 将导入的 xmin 安装到 MyProc->xmin 中
 *
 * 当安装从另一个事务导入的快照时调用该函数。为了确保 OldestXmin 不会向后移动，我们必须检查源事务是否仍在运行，并且我们最好在安装新 xmin 的同时以原子方式执行此操作。
 *
 * 如果成功则返回 true，若源事务不再运行则返回 false。
 */
bool ProcArrayInstallImportedXmin(TransactionId fc_xmin,
							 VirtualTransactionId *fc_sourcevxid)
{
	bool		fc_result = false;
	ProcArrayStruct *fc_arrayP = procArray;
	int			fc_index;

	Assert(TransactionIdIsNormal(fc_xmin));
	if (!fc_sourcevxid)
		return false;

	/* 获取锁，以便源事务在我们执行此操作时无法结束 */
	LWLockAcquire(ProcArrayLock, LW_SHARED);

	for (fc_index = 0; fc_index < fc_arrayP->numProcs; fc_index++)
	{
		int			fc_pgprocno = fc_arrayP->pgprocnos[fc_index];
		PGPROC	   *fc_proc = &allProcs[fc_pgprocno];
		int			fc_statusFlags = ProcGlobal->statusFlags[fc_index];
		TransactionId fc_xid;

		/* 忽略正在运行 LAZY VACUUM 的进程 */
		if (fc_statusFlags & PROC_IN_VACUUM)
			continue;

		
/* 我们只对特定的虚拟事务感兴趣。 */
		if (fc_proc->backendId != fc_sourcevxid->backendId)
			continue;
		if (fc_proc->lxid != fc_sourcevxid->localTransactionId)
			continue;

		/*
		 * 出于偏执的原因，我们检查事务的数据库ID：如果它在另一个数据库中，那么它的xmin不涵盖我们。调用者应该已经检测到这一点，因此我们只将任何奇怪的情况视为“事务未找到”。
		 */
		if (fc_proc->databaseId != MyDatabaseId)
			continue;

		/*
		 * 同样，让我们确保它的xmin确实涵盖我们。
		 */
		fc_xid = UINT32_ACCESS_ONCE(fc_proc->xmin);
		if (!TransactionIdIsNormal(fc_xid) ||
			!TransactionIdPrecedesOrEquals(fc_xid, fc_xmin))
			continue;

		/*
		 * 我们很好。安装新的xmin。就像在GetSnapshotData中一样，也设置TransactionXmin。（请注意，由于snapmgr.c首先调用GetSnapshotData，我们将在这里覆盖一个有效的xmin，因此我们不检查。）
		 */
		MyProc->xmin = TransactionXmin = fc_xmin;

		fc_result = true;
		break;
	}

	LWLockRelease(ProcArrayLock);

	return fc_result;
}

/*
 * ProcArrayInstallRestoredXmin -- 将恢复的xmin安装到MyProc->xmin中
 *
 * 这类似于ProcArrayInstallImportedXmin，但我们有一个指向进口快照的事务的PGPROC的指针，而不是一个XID。
 *
 * 请注意，此函数还会从源`proc`中复制statusFlags，以避免MyProc的xmin在计算xid范围时需要被跳过的情况。
 *
 * 如果成功则返回true，如果源xact不再运行则返回false。
 */
bool ProcArrayInstallRestoredXmin(TransactionId fc_xmin, PGPROC *fc_proc)
{
	bool		fc_result = false;
	TransactionId fc_xid;

	Assert(TransactionIdIsNormal(fc_xmin));
	Assert(fc_proc != NULL);

	/*
	 * 获取一个独占锁，以便我们可以从源proc中复制statusFlags。
	 */
	LWLockAcquire(ProcArrayLock, LW_EXCLUSIVE);

	/*
	 * 确保引用的PGPROC有一个广告的xmin，该xmin不晚于我们正在安装的xmin，以便系统范围的xmin不能回退。此外，确保它在同一个数据库中，以便每个数据库的xmin不能回退。
	 */
	fc_xid = UINT32_ACCESS_ONCE(fc_proc->xmin);
	if (fc_proc->databaseId == MyDatabaseId &&
		TransactionIdIsNormal(fc_xid) &&
		TransactionIdPrecedesOrEquals(fc_xid, fc_xmin))
	{
		/*
		 * 安装 xmin 并传播影响 vacuum 如何解释值的 statusFlags。
		 */
		MyProc->xmin = TransactionXmin = fc_xmin;
		MyProc->statusFlags = (MyProc->statusFlags & ~PROC_XMIN_FLAGS) |
			(fc_proc->statusFlags & PROC_XMIN_FLAGS);
		ProcGlobal->statusFlags[MyProc->pgxactoff] = MyProc->statusFlags;

		fc_result = true;
	}

	LWLockRelease(ProcArrayLock);

	return fc_result;
}

/*
 * GetRunningTransactionData -- 返回有关正在运行的事务的信息。
 *
 * 类似于 GetSnapshotData，但返回更多信息。我们包含
 * 所有分配了 TransactionId 的 PGPROCs，包括 VACUUM 进程和
 * 准备好的事务。
 *
 * 我们获取 XidGenLock 和 ProcArrayLock，但调用者负责
 * 释放它们。获取 XidGenLock 确保在调用者已将此快照
 * 写入 WAL 之前，没有新的 XIDs 进入 proc
 * 数组，并释放锁。获取 ProcArrayLock 确保在
 * 锁释放之前，没有事务提交。
 *
 * 返回的数据结构是静态分配的；调用者不应
 * 修改它，并且必须不假设它在下次调用后仍然有效。
 *
 * 在恢复期间这永远不会执行，因此没有必要查看
 * KnownAssignedXids。
 *
 * 准备事务中的虚拟 PGPROCs 被包含，这意味着这
 * 可能返回具有重复 TransactionId 值的条目，这些值源自
 * 正在完成准备的事务。这里不对重复条目进行处理，以免
 * 不必要地占用 ProcArrayLock。
 *
 * 我们不担心更新其他计数器，我们希望尽可能保持
 * 简单，并将 GetSnapshotData() 留作主要的记账代码。
 *
 * 请注意，如果任何事务溢出了其缓存的子事务，
 * 那么实际上没有必要包含任何子事务。
 */
RunningTransactions
GetRunningTransactionData(void)
{
	/* 结果工作区 */
	static RunningTransactionsData fc_CurrentRunningXactsData;

	ProcArrayStruct *fc_arrayP = procArray;
	TransactionId *fc_other_xids = ProcGlobal->xids;
	RunningTransactions fc_CurrentRunningXacts = &fc_CurrentRunningXactsData;
	TransactionId fc_latestCompletedXid;
	TransactionId fc_oldestRunningXid;
	TransactionId *fc_xids;
	int			fc_index;
	int			fc_count;
	int			fc_subcount;
	bool		fc_suboverflowed;

	Assert(!RecoveryInProgress());

	/*
	 * 为 maxProcs xids 分配空间通常是多余的；numProcs
	 * 就足够了。但是在不持有锁的情况下进行 malloc
	 * 似乎更好，所以我们无法查看 numProcs。
	 * 同样，我们分配的 subxip 存储通常远超所需。
	 *
	 * 应仅在 bgwriter 中分配，因为仅在
	 * 检查点期间执行。
	 */
	if (fc_CurrentRunningXacts->xids == NULL)
	{
		/*
		 * 第一次调用
		 */
		fc_CurrentRunningXacts->xids = (TransactionId *)
			malloc(TOTAL_MAX_CACHED_SUBXIDS * sizeof(TransactionId));
		if (fc_CurrentRunningXacts->xids == NULL)
			ereport(ERROR,
					(errcode(ERRCODE_OUT_OF_MEMORY),
					 errmsg("out of memory")));
	}

	fc_xids = fc_CurrentRunningXacts->xids;

	fc_count = fc_subcount = 0;
	fc_suboverflowed = false;

	/*
	 * 确保在我们获取快照时，没有 xids 进入或离开
	 * procarray。
	 */
	LWLockAcquire(ProcArrayLock, LW_SHARED);
	LWLockAcquire(XidGenLock, LW_SHARED);

	fc_latestCompletedXid =
		XidFromFullTransactionId(ShmemVariableCache->latestCompletedXid);
	fc_oldestRunningXid =
		XidFromFullTransactionId(ShmemVariableCache->nextXid);

	/*
	 * 在 procArray 中循环收集所有 xids
	 */
	for (fc_index = 0; fc_index < fc_arrayP->numProcs; fc_index++)
	{
		TransactionId fc_xid;

		/* 仅获取一次 xid - 参见 GetNewTransactionId */
		fc_xid = UINT32_ACCESS_ONCE(fc_other_xids[fc_index]);

		/*
		 * 我们不需要存储尚未具有 TransactionId 的事务，
		 * 因为它们在备用服务器上不会显示为正在运行。
		 */
		if (!TransactionIdIsValid(fc_xid))
			continue;

		/*
		 * 在计算 oldestRunningXid 和 suboverflowed 的值之前，
		 * 要小心不要排除任何 xids，因为这些用于清理
		 * 保留在备用服务器上的事务信息。
		 */
		if (TransactionIdPrecedes(fc_xid, fc_oldestRunningXid))
			fc_oldestRunningXid = fc_xid;

		if (ProcGlobal->subxidStates[fc_index].overflowed)
			fc_suboverflowed = true;

		/*
		 * 如果我们希望排除 xids，这将是合适的位置。
		 * 设置了 PROC_IN_VACUUM 标志的 Procs 通常不分配 xids，
		 * 但它们在结束时获取锁并截断时会这样做，
		 * 因此如果看到并包括它们也不是什么大问题，包含它们更为干净。
		 */

		fc_xids[fc_count++] = fc_xid;
	}

	/*
	 * 在 procArray 中循环收集所有 subxids，但仅当没有
	 * 出现子溢出时。
	 */
	if (!fc_suboverflowed)
	{
		XidCacheStatus *fc_other_subxidstates = ProcGlobal->subxidStates;

		for (fc_index = 0; fc_index < fc_arrayP->numProcs; fc_index++)
		{
			int			fc_pgprocno = fc_arrayP->pgprocnos[fc_index];
			PGPROC	   *fc_proc = &allProcs[fc_pgprocno];
			int			fc_nsubxids;

			/*
			 * 保存子事务 XIDs。其他后端不能在我们持有
			 * XidGenLock 时添加或删除条目。
			 */
			fc_nsubxids = fc_other_subxidstates[fc_index].count;
			if (fc_nsubxids > 0)
			{
				/* 屏障实际上不是必需的，因为持有着 XidGenLock，但... */
				pg_read_barrier();	/* 与 GetNewTransactionId 配对 */

				memcpy(&fc_xids[fc_count], (void *) fc_proc->subxids.xids,
					   fc_nsubxids * sizeof(TransactionId));
				fc_count += fc_nsubxids;
				fc_subcount += fc_nsubxids;

				/*
				 * 事务的顶层 XID 始终小于其任何子 xids，
				 * 因此我们不需要检查任何子 xids 是否小于
				 * oldestRunningXid
				 */
			}
		}
	}

	/*
	 * 重要的是 *不要* 包含这里由插槽设置的限制，
	 * 因为 snapbuild.c 使用 oldestRunningXid 来管理
	 * 其 xmin 地平线。如果将这些包含在这里，则初始值
	 * 永远无法增加，因为存在循环依赖，其中插槽仅在
	 * 正在运行的 xacts 增加 oldestRunningXid 时才增加其限制，
	 * 而正在运行的 xacts 仅在插槽执行时才增加。
	 */

	fc_CurrentRunningXacts->xcnt = fc_count - fc_subcount;
	fc_CurrentRunningXacts->subxcnt = fc_subcount;
	fc_CurrentRunningXacts->subxid_status = fc_suboverflowed ? SUBXIDS_IN_SUBTRANS : SUBXIDS_IN_ARRAY;
	fc_CurrentRunningXacts->nextXid = XidFromFullTransactionId(ShmemVariableCache->nextXid);
	fc_CurrentRunningXacts->oldestRunningXid = fc_oldestRunningXid;
	fc_CurrentRunningXacts->latestCompletedXid = fc_latestCompletedXid;

	Assert(TransactionIdIsValid(fc_CurrentRunningXacts->nextXid));
	Assert(TransactionIdIsValid(fc_CurrentRunningXacts->oldestRunningXid));
	Assert(TransactionIdIsNormal(fc_CurrentRunningXacts->latestCompletedXid));

	/* 我们在这里不释放锁，调用者负责任何释放 */

	return fc_CurrentRunningXacts;
}

/*
 * GetOldestActiveTransactionId()
 *
 * 类似于 GetSnapshotData，但仅返回 oldestActiveXid。
 * 我们包括所有分配了 TransactionId 的 PGPROCs，包括
 * VACUUM 进程。我们查看所有数据库，尽管没有必要
 * 包含 WALSender，因为这对热备用冲突没有影响。
 *
 * 在恢复期间这永远不会执行，因此没有必要查看
 * KnownAssignedXids。
 *
 * 我们不担心更新其他计数器，我们希望尽可能保持
 * 简单，并将 GetSnapshotData() 留作主要的记账代码。
 */
TransactionId GetOldestActiveTransactionId(void)
{
	ProcArrayStruct *fc_arrayP = procArray;
	TransactionId *fc_other_xids = ProcGlobal->xids;
	TransactionId fc_oldestRunningXid;
	int			fc_index;

	Assert(!RecoveryInProgress());

	/*
	 * 读取 nextXid，作为仍然活动内容的上限。
	 *
	 * 读取 TransactionId 是原子的，但我们必须获取锁以确保
	 * 在我们循环检查时，所有 < nextXid 的 XIDs
	 * 已经在 proc 数组中（或已经完成）。
	 */
	LWLockAcquire(XidGenLock, LW_SHARED);
	fc_oldestRunningXid = XidFromFullTransactionId(ShmemVariableCache->nextXid);
	LWLockRelease(XidGenLock);

	/*
	 * 在 procArray 中循环收集所有 xids 和 subxids。
	 */
	LWLockAcquire(ProcArrayLock, LW_SHARED);
	for (fc_index = 0; fc_index < fc_arrayP->numProcs; fc_index++)
	{
		TransactionId fc_xid;

		/* 仅获取一次 xid - 参见 GetNewTransactionId */
		fc_xid = UINT32_ACCESS_ONCE(fc_other_xids[fc_index]);

		if (!TransactionIdIsNormal(fc_xid))
			continue;

		if (TransactionIdPrecedes(fc_xid, fc_oldestRunningXid))
			fc_oldestRunningXid = fc_xid;

		
/*
		 * 事务的顶级 XID 总是小于其任何子 XID，因此我们不需要检查任何子 XID 是否小于 oldestRunningXid
		 */
	}
	LWLockRelease(ProcArrayLock);

	return fc_oldestRunningXid;
}

/*
 * GetOldestSafeDecodingTransactionId -- 最低的未受清理影响的 XID
 *
 * 返回我们可以保证未被清理影响的最旧 XID，即没有行 >= 该 XID 被清理掉，除非事务被中止。请注意，值可能（且大部分时候会）比实际受到清理影响的更为保守，但我们目前没有更好的数据可用。
 *
 * 这对于初始化一个新的变更集提取复制槽的截止 XID 很有用。
 *
 * 必须在持有 ProcArrayLock 的共享或独占模式下调用，尽管大多数调用者会希望使用独占模式，因为预期调用者会立即使用该 XID 来固定 xmin 数据范围。
 */
TransactionId GetOldestSafeDecodingTransactionId(bool fc_catalogOnly)
{
	ProcArrayStruct *fc_arrayP = procArray;
	TransactionId fc_oldestSafeXid;
	int			fc_index;
	bool		fc_recovery_in_progress = RecoveryInProgress();

	Assert(LWLockHeldByMe(ProcArrayLock));

	/*
	 * 获取 XidGenLock，这样在我们运行时没有事务能够获取 XID。如果没有与 XID 并发运行的事务，新 XID 可能会影响 RecentXmin 等数据。
	 *
	 * 我们将计算初始化为 nextXid，因为这是一个保证安全的，尽管是最差的值。
	 */
	LWLockAcquire(XidGenLock, LW_SHARED);
	fc_oldestSafeXid = XidFromFullTransactionId(ShmemVariableCache->nextXid);

	/*
	 * 如果已经有一个槽固定了 xmin 数据范围，我们可以从该值开始，它是安全的，因为最初是由此例程计算的，并且从那时起一直得到了维护。我们总是可以使用该槽的一般 xmin 数据范围，但目录数据范围仅在只查看目录数据时可用。
	 */
	if (TransactionIdIsValid(procArray->replication_slot_xmin) &&
		TransactionIdPrecedes(procArray->replication_slot_xmin,
							  fc_oldestSafeXid))
		fc_oldestSafeXid = procArray->replication_slot_xmin;

	if (fc_catalogOnly &&
		TransactionIdIsValid(procArray->replication_slot_catalog_xmin) &&
		TransactionIdPrecedes(procArray->replication_slot_catalog_xmin,
							  fc_oldestSafeXid))
		fc_oldestSafeXid = procArray->replication_slot_catalog_xmin;

	/*
	 * 如果我们不在恢复中，我们会遍历 procarray 并收集最低的 XID。由于我们在持有 ProcArrayLock 时调用，并且获取了 XidGenLock，因此没有条目可以并发消失，因为 ProcGlobal->xids[i] 仅在持有 XidGenLock 时设置，并且仅在持有 ProcArrayLock 时清除。
	 *
	 * 在恢复中，我们不能降低安全值，除了我们已计算的值，因此我们必须在这里等待更长时间。不幸的是，我们不能使用 KnownAssignedXidsGetOldestXmin()，因为 KnownAssignedXids 机制可能会错过某些值并返回一个比安全值更旧的值。
	 */
	if (!fc_recovery_in_progress)
	{
		TransactionId *fc_other_xids = ProcGlobal->xids;

		/*
		 * 在 procArray 上进行轮询，收集 min(ProcGlobal->xids[i])
		 */
		for (fc_index = 0; fc_index < fc_arrayP->numProcs; fc_index++)
		{
			TransactionId fc_xid;

			/* 仅获取一次 xid - 参见 GetNewTransactionId */
			fc_xid = UINT32_ACCESS_ONCE(fc_other_xids[fc_index]);

			if (!TransactionIdIsNormal(fc_xid))
				continue;

			if (TransactionIdPrecedes(fc_xid, fc_oldestSafeXid))
				fc_oldestSafeXid = fc_xid;
		}
	}

	LWLockRelease(XidGenLock);

	return fc_oldestSafeXid;
}

/*
 * GetVirtualXIDsDelayingChkpt -- 获取因有关键操作正在进行而延迟检查点的事务的 VXIDs。
 *
 * 构建一个当前处于提交关键区段的事务的 VXID 数组，如其 PGPROC 中设置的 delayChkptFlags 位所示。
 *
 * 返回一个 palloc 的数组，调用者应当释放。
 * *nvxids 是有效条目的数量。
 *
 * 请注意，因为后端在未持有任何锁的情况下设置或清除 delayChkptFlags，结果是有些不确定的，但我们并不在乎。即使在一个多处理器系统中，对共享内存的延迟写入，设置 delayChkptFlags 时，后端获取锁时会传播到共享内存，因此我们不能错误地将虚拟事务视为 delayChkptFlags，如果它已经插入了其提交记录。无论清除 delayChkptFlags 传播需要多长时间对于正确性来说都不重要。
 */
VirtualTransactionId * GetVirtualXIDsDelayingChkpt(int *fc_nvxids, int fc_type)
{
	VirtualTransactionId *fc_vxids;
	ProcArrayStruct *fc_arrayP = procArray;
	int			fc_count = 0;
	int			fc_index;

	Assert(fc_type != 0);

	/* 分配足够的结果空间 */
	fc_vxids = (VirtualTransactionId *)
		palloc(sizeof(VirtualTransactionId) * fc_arrayP->maxProcs);

	LWLockAcquire(ProcArrayLock, LW_SHARED);

	for (fc_index = 0; fc_index < fc_arrayP->numProcs; fc_index++)
	{
		int			fc_pgprocno = fc_arrayP->pgprocnos[fc_index];
		PGPROC	   *fc_proc = &allProcs[fc_pgprocno];

		if ((fc_proc->delayChkptFlags & fc_type) != 0)
		{
			VirtualTransactionId fc_vxid;

			GET_VXID_FROM_PGPROC(fc_vxid, *fc_proc);
			if (VirtualTransactionIdIsValid(fc_vxid))
				fc_vxids[fc_count++] = fc_vxid;
		}
	}

	LWLockRelease(ProcArrayLock);

	*fc_nvxids = fc_count;
	return fc_vxids;
}

/*
 * HaveVirtualXIDsDelayingChkpt -- 指定的 VXIDs 中是否有任何正在延迟？
 *
 * 这与 GetVirtualXIDsDelayingChkpt 的结果一起使用，以查看指定的 VXIDs 中是否还有在代码的关键区段中。
 *
 * 注意：这是 O(N^2) 复杂度，涉及正在延迟的 vxacts 数量，但这些数量应该足够小，因此不会形成问题。
 */
bool HaveVirtualXIDsDelayingChkpt(VirtualTransactionId *fc_vxids, int fc_nvxids, int fc_type)
{
	bool		fc_result = false;
	ProcArrayStruct *fc_arrayP = procArray;
	int			fc_index;

	Assert(fc_type != 0);

	LWLockAcquire(ProcArrayLock, LW_SHARED);

	for (fc_index = 0; fc_index < fc_arrayP->numProcs; fc_index++)
	{
		int			fc_pgprocno = fc_arrayP->pgprocnos[fc_index];
		PGPROC	   *fc_proc = &allProcs[fc_pgprocno];
		VirtualTransactionId fc_vxid;

		GET_VXID_FROM_PGPROC(fc_vxid, *fc_proc);

		if ((fc_proc->delayChkptFlags & fc_type) != 0 &&
			VirtualTransactionIdIsValid(fc_vxid))
		{
			int			fc_i;

			for (fc_i = 0; fc_i < fc_nvxids; fc_i++)
			{
				if (VirtualTransactionIdEquals(fc_vxid, fc_vxids[fc_i]))
				{
					fc_result = true;
					break;
				}
			}
			if (fc_result)
				break;
		}
	}

	LWLockRelease(ProcArrayLock);

	return fc_result;
}

/*
 * BackendPidGetProc -- 根据其 PID 获取后端的 PGPROC
 *
 * 如果未找到则返回 NULL。请注意，确保该问题在足够长的时间内仍然有意义以便使用答案由调用者负责...
 */
PGPROC * BackendPidGetProc(int fc_pid)
{
	PGPROC	   *fc_result;

	if (fc_pid == 0)				/* 永远不匹配虚拟 PGPROCs */
		return NULL;

	LWLockAcquire(ProcArrayLock, LW_SHARED);

	fc_result = BackendPidGetProcWithLock(fc_pid);

	LWLockRelease(ProcArrayLock);

	return fc_result;
}

/*
 * BackendPidGetProcWithLock -- 获取后端的 PGPROC，给定其 PID
 *
 * 与上述相同，除了调用者必须持有 ProcArrayLock。找到的条目（如果有）可以假定在锁保持期间是有效的。
 */
PGPROC * BackendPidGetProcWithLock(int fc_pid)
{
	PGPROC	   *fc_result = NULL;
	ProcArrayStruct *fc_arrayP = procArray;
	int			fc_index;

	if (fc_pid == 0)				/* 永远不匹配虚拟 PGPROCs */
		return NULL;

	for (fc_index = 0; fc_index < fc_arrayP->numProcs; fc_index++)
	{
		PGPROC	   *fc_proc = &allProcs[fc_arrayP->pgprocnos[fc_index]];

		if (fc_proc->pid == fc_pid)
		{
			fc_result = fc_proc;
			break;
		}
	}

	return fc_result;
}

/*
 * BackendXidGetPid -- 获取后端的 pid，给定其 XID
 *
 * 如果未找到或这是一个准备好的事务，则返回 0。注意，确保问题在足够长的时间内保持意义，以便使用答案是调用者的责任...
 *
 * 仅考虑主要事务 Id。此函数主要用于确定哪个后端拥有锁。
 *
 * 请注意，并非每个事务都有分配的 XID。然而，只要您使用在磁盘上找到的 XID 调用此函数，您就安全。
 */
int BackendXidGetPid(TransactionId fc_xid)
{
	int			fc_result = 0;
	ProcArrayStruct *fc_arrayP = procArray;
	TransactionId *fc_other_xids = ProcGlobal->xids;
	int			fc_index;

	if (fc_xid == InvalidTransactionId)	/* 永远不会匹配无效的 xid */
		return 0;

	LWLockAcquire(ProcArrayLock, LW_SHARED);

	for (fc_index = 0; fc_index < fc_arrayP->numProcs; fc_index++)
	{
		int			fc_pgprocno = fc_arrayP->pgprocnos[fc_index];
		PGPROC	   *fc_proc = &allProcs[fc_pgprocno];

		if (fc_other_xids[fc_index] == fc_xid)
		{
			fc_result = fc_proc->pid;
			break;
		}
	}

	LWLockRelease(ProcArrayLock);

	return fc_result;
}

/*
 * IsBackendPid -- 给定的 pid 是否是一个正在运行的后端
 *
 * 此函数不被后端调用，但由外部模块调用。
 */
bool IsBackendPid(int fc_pid)
{
	return (BackendPidGetProc(fc_pid) != NULL);
}


/*
 * GetCurrentVirtualXIDs -- 返回当前活动的 VXIDs 数组。
 *
 * 数组是通过 palloc 分配的。有效条目的数量返回到 *nvxids。
 *
 * 参数允许过滤返回的 VXIDs 集合。我们的进程始终被跳过。此外：
 *	如果 limitXmin 不是 InvalidTransactionId，则跳过 xmin > limitXmin 的进程。
 *	如果 excludeXmin0 为真，则跳过 xmin = 0 的进程。
 *	如果 allDbs 为假，则跳过连接到其他数据库的进程。
 *	如果 excludeVacuum 不为零，则跳过 (statusFlags & excludeVacuum) 不为零的进程。
 *
 * 注意：limitXmin 和 excludeXmin0 参数的目的是允许跳过其最旧的活动快照不比我们拥有的快照更旧的后端。由于我们仅使用共享锁检查 procarray，因此存在竞争条件：一个后端可能会在我们查看后立即设置其 xmin。确实，在具有弱内存排序的多处理器上，其他后端可能在我们查看之前设置了其 xmin。但是我们知道，这样的后端必须在我们持有 ProcArrayLock 期间持有共享 ProcArrayLock，否则我们会看到其 xmin 更新。因此，其他后端与我们的扫描同时进行时所拍摄的任何快照无法考虑任何我们认为已提交的事务（因为后端必须独占持有 ProcArrayLock 才能提交）。
 */
VirtualTransactionId * GetCurrentVirtualXIDs(TransactionId fc_limitXmin, bool fc_excludeXmin0,
					  bool fc_allDbs, int fc_excludeVacuum,
					  int *fc_nvxids)
{
	VirtualTransactionId *fc_vxids;
	ProcArrayStruct *fc_arrayP = procArray;
	int			fc_count = 0;
	int			fc_index;

	/* 分配足够的结果空间 */
	fc_vxids = (VirtualTransactionId *)
		palloc(sizeof(VirtualTransactionId) * fc_arrayP->maxProcs);

	LWLockAcquire(ProcArrayLock, LW_SHARED);

	for (fc_index = 0; fc_index < fc_arrayP->numProcs; fc_index++)
	{
		int			fc_pgprocno = fc_arrayP->pgprocnos[fc_index];
		PGPROC	   *fc_proc = &allProcs[fc_pgprocno];
		uint8		fc_statusFlags = ProcGlobal->statusFlags[fc_index];

		if (fc_proc == MyProc)
			continue;

		if (fc_excludeVacuum & fc_statusFlags)
			continue;

		if (fc_allDbs || fc_proc->databaseId == MyDatabaseId)
		{
			/* 仅获取一次 xmin - 可能会发生更改 */
			TransactionId fc_pxmin = UINT32_ACCESS_ONCE(fc_proc->xmin);

			if (fc_excludeXmin0 && !TransactionIdIsValid(fc_pxmin))
				continue;

			/*
			 * InvalidTransactionId 在所有其他 XID 之前，因此尚未设置 xmin 的进程不会被此测试拒绝。
			 */
			if (!TransactionIdIsValid(fc_limitXmin) ||
				TransactionIdPrecedesOrEquals(fc_pxmin, fc_limitXmin))
			{
				VirtualTransactionId fc_vxid;

				GET_VXID_FROM_PGPROC(fc_vxid, *fc_proc);
				if (VirtualTransactionIdIsValid(fc_vxid))
					fc_vxids[fc_count++] = fc_vxid;
			}
		}
	}

	LWLockRelease(ProcArrayLock);

	*fc_nvxids = fc_count;
	return fc_vxids;
}

/*
 * GetConflictingVirtualXIDs -- 返回当前活动的 VXIDs 数组。
 *
 * 用于恢复期间的冲突解决，限于备用服务器。
 * limitXmin 被提供为最新移除的 Xid，或在无法准确确定最新移除的 Xid 值的情况下为 InvalidTransactionId。
 *
 * 如果 limitXmin 为 InvalidTransactionId，则我们希望杀死所有人，因此不担心他们是否具有快照，也不管我们持有什么类型的锁。
 *
 * 所有检查 xmins 的调用者现在始终提供有效和有用的 limitXmin 值。limitXmin 总是低于未被 FATAL 错误占用的已知分配 Xid 中最小的编号。这是因为我们只关心清理正在清理已提交事务的元组版本的清理记录。在这种情况下，它们只会在记录小于最低运行 xid 的点发生。这使我们能够说，如果任何后端与我们同时拍摄快照，则此处所做的冲突评估将决不会包括正在推导的快照。因此，我们对 ProcArray 采取 LW_SHARED，允许当 limitXmin 有效时并发快照。我们可能会考虑添加
 *	 Assert(limitXmin < lowest(KnownAssignedXids))
 * 但在 FATAL 错误滞后于数组的情况下这将不成立，但我们已经知道这些都是虚假的，因此我们跳过该测试。
 *
 * 如果 dbOid 有效，我们跳过连接到其他数据库的后端。
 *
 * 注意 *不* pfree 此函数的结果。我们重复使用此数组的频率足够高，因此使用 malloc 作为结果。
 */
VirtualTransactionId * GetConflictingVirtualXIDs(TransactionId fc_limitXmin, Oid fc_dbOid)
{
	static VirtualTransactionId *fc_vxids;
	ProcArrayStruct *fc_arrayP = procArray;
	int			fc_count = 0;
	int			fc_index;

	/*
	 * 如果第一次执行，则获取工作区以记住主要 XIDs。我们永久分配它以避免重复的 palloc/pfree 开销。允许结果空间，记住终止符的空间。
	 */
	if (fc_vxids == NULL)
	{
		fc_vxids = (VirtualTransactionId *)
			malloc(sizeof(VirtualTransactionId) * (fc_arrayP->maxProcs + 1));
		if (fc_vxids == NULL)
			ereport(ERROR,
					(errcode(ERRCODE_OUT_OF_MEMORY),
					 errmsg("out of memory")));
	}

	LWLockAcquire(ProcArrayLock, LW_SHARED);

	for (fc_index = 0; fc_index < fc_arrayP->numProcs; fc_index++)
	{
		int			fc_pgprocno = fc_arrayP->pgprocnos[fc_index];
		PGPROC	   *fc_proc = &allProcs[fc_pgprocno];

		/* 排除准备好的事务 */
		if (fc_proc->pid == 0)
			continue;

		if (!OidIsValid(fc_dbOid) ||
			fc_proc->databaseId == fc_dbOid)
		{
			/* 仅获取一次 xmin - 不能更改，但好的编码 */
			TransactionId fc_pxmin = UINT32_ACCESS_ONCE(fc_proc->xmin);

			
/*
			 * 我们忽略无效的 pxmin，因为这意味着后端当前没有
			 * 快照。我们持有一个共享锁，以避免与正在获取快照的用户
			 * 发生竞争。这不是问题，因为当前的 xmin 始终至少比最新的
			 * 已移除 xid 大一，因此任何新的快照都不会与这里的测试冲突。
			 */
			if (!TransactionIdIsValid(fc_limitXmin) ||
				(TransactionIdIsValid(fc_pxmin) && !TransactionIdFollows(fc_pxmin, fc_limitXmin)))
			{
				VirtualTransactionId fc_vxid;

				GET_VXID_FROM_PGPROC(fc_vxid, *fc_proc);
				if (VirtualTransactionIdIsValid(fc_vxid))
					fc_vxids[fc_count++] = fc_vxid;
			}
		}
	}

	LWLockRelease(ProcArrayLock);

	/* 添加终止符 */
	fc_vxids[fc_count].backendId = InvalidBackendId;
	fc_vxids[fc_count].localTransactionId = InvalidLocalTransactionId;

	return fc_vxids;
}

/*
 * CancelVirtualTransaction - 用于恢复冲突处理
 *
 * 返回被发信号的进程的 pid，如果未找到则返回 0。
 */
pid_t CancelVirtualTransaction(VirtualTransactionId fc_vxid, ProcSignalReason fc_sigmode)
{
	return SignalVirtualTransaction(fc_vxid, fc_sigmode, true);
}

pid_t SignalVirtualTransaction(VirtualTransactionId fc_vxid, ProcSignalReason fc_sigmode,
						 bool fc_conflictPending)
{
	ProcArrayStruct *fc_arrayP = procArray;
	int			fc_index;
	pid_t		fc_pid = 0;

	LWLockAcquire(ProcArrayLock, LW_SHARED);

	for (fc_index = 0; fc_index < fc_arrayP->numProcs; fc_index++)
	{
		int			fc_pgprocno = fc_arrayP->pgprocnos[fc_index];
		PGPROC	   *fc_proc = &allProcs[fc_pgprocno];
		VirtualTransactionId fc_procvxid;

		GET_VXID_FROM_PGPROC(fc_procvxid, *fc_proc);

		if (fc_procvxid.backendId == fc_vxid.backendId &&
			fc_procvxid.localTransactionId == fc_vxid.localTransactionId)
		{
			fc_proc->recoveryConflictPending = fc_conflictPending;
			fc_pid = fc_proc->pid;
			if (fc_pid != 0)
			{
				/*
				 * 如果 pid 仍在这里，杀死它。如果没有，那正是我们
				 * 所希望的，因此忽略任何错误。
				 */
				(void) SendProcSignal(fc_pid, fc_sigmode, fc_vxid.backendId);
			}
			break;
		}
	}

	LWLockRelease(ProcArrayLock);

	return fc_pid;
}

/*
 * MinimumActiveBackends --- 计算处于活动事务中的后端（除了我自己）。
 *	如果计数超过传入的最小阈值，则返回真。这被用作一个启发式
 *	方法，以决定在提交期间是否值得进行 XLOG 刷新延迟。
 *
 * 不要计算等待锁被阻塞的后端，因为它们在其他人提交之前不会被
 * 运行。
 */
bool MinimumActiveBackends(int fc_min)
{
	ProcArrayStruct *fc_arrayP = procArray;
	int			fc_count = 0;
	int			fc_index;

	/* 如果未指定最小值，则快速短路 */
	if (fc_min == 0)
		return true;

	/*
	 * 注意：为了速度，我们不获取 ProcArrayLock。这有点
	 * 不太理想，但由于我们只是测试字段是否为零或非零，
	 * 所以应该没问题。结果仅用于启发式目的……
	 */
	for (fc_index = 0; fc_index < fc_arrayP->numProcs; fc_index++)
	{
		int			fc_pgprocno = fc_arrayP->pgprocnos[fc_index];
		PGPROC	   *fc_proc = &allProcs[fc_pgprocno];

		/*
		 * 由于我们没有持有锁，因此需要准备处理垃圾，因为某人
		 * 可能已经递增了 numProcs，但尚未填充结构。
		 *
		 * 如果有人刚刚递减了 numProcs，'proc' 也可能指向一个
		 * 不再在数组中的 PGPROC 条目。不过，它仍指向一个
		 * PGPROC 结构，因为被释放的 PGPROC 条目会直接进入
		 * 空闲列表并被回收。在这种情况下，它的内容是无意义的，
		 * 但对于这个函数来说这是可以接受的。
		 */
		if (fc_pgprocno == -1)
			continue;			/* 不要计算已删除的条目 */
		if (fc_proc == MyProc)
			continue;			/* 不要计算我自己 */
		if (fc_proc->xid == InvalidTransactionId)
			continue;			/* 如果没有分配 XID，则不计数 */
		if (fc_proc->pid == 0)
			continue;			/* 不要计算准备好的事务 */
		if (fc_proc->waitLock != NULL)
			continue;			/* 如果被锁阻塞，则不计数 */
		fc_count++;
		if (fc_count >= fc_min)
			break;
	}

	return fc_count >= fc_min;
}

/*
 * CountDBBackends --- 计算使用指定数据库的后端
 */
int CountDBBackends(Oid fc_databaseid)
{
	ProcArrayStruct *fc_arrayP = procArray;
	int			fc_count = 0;
	int			fc_index;

	LWLockAcquire(ProcArrayLock, LW_SHARED);

	for (fc_index = 0; fc_index < fc_arrayP->numProcs; fc_index++)
	{
		int			fc_pgprocno = fc_arrayP->pgprocnos[fc_index];
		PGPROC	   *fc_proc = &allProcs[fc_pgprocno];

		if (fc_proc->pid == 0)
			continue;			/* 不要计算准备好的事务 */
		if (!OidIsValid(fc_databaseid) ||
			fc_proc->databaseId == fc_databaseid)
			fc_count++;
	}

	LWLockRelease(ProcArrayLock);

	return fc_count;
}

/*
 * CountDBConnections --- 计算数据库后端（仅普通后端）
 */
int CountDBConnections(Oid fc_databaseid)
{
	ProcArrayStruct *fc_arrayP = procArray;
	int			fc_count = 0;
	int			fc_index;

	LWLockAcquire(ProcArrayLock, LW_SHARED);

	for (fc_index = 0; fc_index < fc_arrayP->numProcs; fc_index++)
	{
		int			fc_pgprocno = fc_arrayP->pgprocnos[fc_index];
		PGPROC	   *fc_proc = &allProcs[fc_pgprocno];

		if (fc_proc->pid == 0)
			continue;			/* 不要计算准备好的事务 */
		if (fc_proc->isBackgroundWorker)
			continue;			/* 不要计算后台工作进程 */
		if (!OidIsValid(fc_databaseid) ||
			fc_proc->databaseId == fc_databaseid)
			fc_count++;
	}

	LWLockRelease(ProcArrayLock);

	return fc_count;
}

/*
 * CancelDBBackends --- 取消使用指定数据库的后端
 */
void CancelDBBackends(Oid fc_databaseid, ProcSignalReason fc_sigmode, bool fc_conflictPending)
{
	ProcArrayStruct *fc_arrayP = procArray;
	int			fc_index;

	/* 告诉所有后端退出 */
	LWLockAcquire(ProcArrayLock, LW_EXCLUSIVE);

	for (fc_index = 0; fc_index < fc_arrayP->numProcs; fc_index++)
	{
		int			fc_pgprocno = fc_arrayP->pgprocnos[fc_index];
		PGPROC	   *fc_proc = &allProcs[fc_pgprocno];

		if (fc_databaseid == InvalidOid || fc_proc->databaseId == fc_databaseid)
		{
			VirtualTransactionId fc_procvxid;
			pid_t		fc_pid;

			GET_VXID_FROM_PGPROC(fc_procvxid, *fc_proc);

			fc_proc->recoveryConflictPending = fc_conflictPending;
			fc_pid = fc_proc->pid;
			if (fc_pid != 0)
			{
				/*
				 * 如果 pid 仍在这里，杀死它。如果没有，那正是我们
				 * 所希望的，因此忽略任何错误。
				 */
				(void) SendProcSignal(fc_pid, fc_sigmode, fc_procvxid.backendId);
			}
		}
	}

	LWLockRelease(ProcArrayLock);
}

/*
 * CountUserBackends --- 计算指定用户使用的后端数量
 * (仅常规后端，不包括任何类型的后台工作线程)
 */
int CountUserBackends(Oid fc_roleid)
{
	ProcArrayStruct *fc_arrayP = procArray;
	int			fc_count = 0;
	int			fc_index;

	LWLockAcquire(ProcArrayLock, LW_SHARED);

	for (fc_index = 0; fc_index < fc_arrayP->numProcs; fc_index++)
	{
		int			fc_pgprocno = fc_arrayP->pgprocnos[fc_index];
		PGPROC	   *fc_proc = &allProcs[fc_pgprocno];

		if (fc_proc->pid == 0)
			continue;			/* 不要计算准备好的事务 */
		if (fc_proc->isBackgroundWorker)
			continue;			/* 不要计算后台工作进程 */
		if (fc_proc->roleId == fc_roleid)
			fc_count++;
	}

	LWLockRelease(ProcArrayLock);

	return fc_count;
}

/*
 * CountOtherDBBackends -- 检查在给定数据库中运行的其他后端
 *
 * 如果数据库中有其他后端，我们将等待最多 5 秒
 * 以让它们退出。通过发送 SIGTERM 来鼓励自动真空后端尽早退出，
 * 但正常用户后端则只是等待。
 *
 * 当前后端总是被忽略；调用者有责任
 * 检查当前后端是否使用了给定数据库，如果这很重要。
 *
 * 如果数据库中还有（仍然）其他后端，则返回 true，否则返回 false。
 * 此外，*nbackends 和 *nprepared 被设置为数据库中其他后端
 * 和准备好的事务的数量，分别。
 *
 * 此函数用于在目标数据库中对 DROP DATABASE 和相关命令进行互锁
 * 以防存在任何活动的后端——在活动后端仍然存在的情况下删除
 * 数据库将是一个坏事。请注意，我们无法
 * 在这里检测到正在尝试连接到即将被删除数据库的新启动后端的可能性，
 * 因此在后端启动期间需要额外的互锁。调用者通常应该在调用此函数之前
 * 对目标数据库持有排他锁，这也是我们不能无限期等待的一个原因。
 */
bool CountOtherDBBackends(Oid fc_databaseId, int *fc_nbackends, int *fc_nprepared)
{
	ProcArrayStruct *fc_arrayP = procArray;

#define MAXAUTOVACPIDS	10		/* 每次迭代最多 SIGTERM 发送给的自动真空数量 */
	int			fc_autovac_pids[MAXAUTOVACPIDS];
	int			fc_tries;

	/* 50 次尝试，每次尝试之间睡眠 100 毫秒，总共等待 5 秒 */
	for (fc_tries = 0; fc_tries < 50; fc_tries++)
	{
		int			fc_nautovacs = 0;
		bool		fc_found = false;
		int			fc_index;

		CHECK_FOR_INTERRUPTS();

		*fc_nbackends = *fc_nprepared = 0;

		LWLockAcquire(ProcArrayLock, LW_SHARED);

		for (fc_index = 0; fc_index < fc_arrayP->numProcs; fc_index++)
		{
			int			fc_pgprocno = fc_arrayP->pgprocnos[fc_index];
			PGPROC	   *fc_proc = &allProcs[fc_pgprocno];
			uint8		fc_statusFlags = ProcGlobal->statusFlags[fc_index];

			if (fc_proc->databaseId != fc_databaseId)
				continue;
			if (fc_proc == MyProc)
				continue;

			fc_found = true;

			if (fc_proc->pid == 0)
				(*fc_nprepared)++;
			else
			{
				(*fc_nbackends)++;
				if ((fc_statusFlags & PROC_IS_AUTOVACUUM) &&
					fc_nautovacs < MAXAUTOVACPIDS)
					fc_autovac_pids[fc_nautovacs++] = fc_proc->pid;
			}
		}

		LWLockRelease(ProcArrayLock);

		if (!fc_found)
			return false;		/* 没有冲突的后端，所以完成 */

		/*
		 * 在睡眠之前向任何冲突的自动真空发送 SIGTERM。我们
		 * 将这一步推迟到循环之后，因为我们不想在发出 kill() 时
		 * 持有 ProcArrayLock。我们不知道在内核内部可能会有什么阻塞
		 * kill()...
		 */
		for (fc_index = 0; fc_index < fc_nautovacs; fc_index++)
			(void) kill(fc_autovac_pids[fc_index], SIGTERM);	/* 忽略任何错误 */

		/* 睡眠，然后再试一次 */
		pg_usleep(100 * 1000L); /* 100ms */
	}

	return true;				/* 超时，仍有冲突 */
}

/*
 * 终止与指定数据库的现有连接。该例程
 * 在用户要求强制删除数据库时由 DROP DATABASE 命令使用。
 *
 * 当前后端总是被忽略；调用者有责任
 * 检查当前后端是否使用了给定数据库，如果这很重要。
 *
 * 如果目标数据库有一个准备的事务或连接的权限检查
 * 失败，则此操作会失败，但不会终止任何内容。
 */
void TerminateOtherDBBackends(Oid fc_databaseId)
{
	ProcArrayStruct *fc_arrayP = procArray;
	List	   *fc_pids = NIL;
	int			fc_nprepared = 0;
	int			fc_i;

	LWLockAcquire(ProcArrayLock, LW_SHARED);

	for (fc_i = 0; fc_i < procArray->numProcs; fc_i++)
	{
		int			fc_pgprocno = fc_arrayP->pgprocnos[fc_i];
		PGPROC	   *fc_proc = &allProcs[fc_pgprocno];

		if (fc_proc->databaseId != fc_databaseId)
			continue;
		if (fc_proc == MyProc)
			continue;

		if (fc_proc->pid != 0)
			fc_pids = lappend_int(fc_pids, fc_proc->pid);
		else
			fc_nprepared++;
	}

	LWLockRelease(ProcArrayLock);

	if (fc_nprepared > 0)
		ereport(ERROR,
				(errcode(ERRCODE_OBJECT_IN_USE),
				 errmsg("database \"%s\" is being used by prepared transactions",
						get_database_name(fc_databaseId)),
				 errdetail_plural("There is %d prepared transaction using the database.",
								  "There are %d prepared transactions using the database.",
								  fc_nprepared,
								  fc_nprepared)));

	if (fc_pids)
	{
		ListCell   *fc_lc;

		/*
		 * 权限检查以两种方式放宽 pg_terminate_backend 检查，
		 * 通过省略 !OidIsValid(proc->roleId) 检查：
		 *
		 * - 接受终止自动真空工作线程，因为没有 FORCE
		 * 的 DROP DATABASE 会终止它们。
		 *
		 * - 接受终止 bgworkers。对于 bgworker 的作者来说，
		 * 如果工作线程意外阻塞 DROP DATABASE，能够推荐 FORCE 是
		 * 很方便的。
		 *
		 * 与 pg_terminate_backend 不同，我们不会引发某些警告 —— 比如
		 * “PID %d 不是 PostgreSQL 服务器进程”，因为对我们来说
		 * 已经完成的会话不是问题。
		 */
		foreach(fc_lc, fc_pids)
		{
			int			fc_pid = lfirst_int(fc_lc);
			PGPROC	   *fc_proc = BackendPidGetProc(fc_pid);

			if (fc_proc != NULL)
			{
				if (superuser_arg(fc_proc->roleId) && !superuser())
					ereport(ERROR,
							(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
							 errmsg("must be a superuser to terminate superuser process")));

				if (!has_privs_of_role(GetUserId(), fc_proc->roleId) &&
					!has_privs_of_role(GetUserId(), ROLE_PG_SIGNAL_BACKEND))
					ereport(ERROR,
							(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
							 errmsg("must be a member of the role whose process is being terminated or member of pg_signal_backend")));
			}
		}

		/*
		 * 这里存在竞争条件：一旦我们释放了 ProcArrayLock，
		 * 会话可能会在我们发出 kill 之前退出。这个
		 * 竞争条件的可能性似乎太小而不值得担心。见
		 * pg_signal_backend。
		 */
		foreach(fc_lc, fc_pids)
		{
			int			fc_pid = lfirst_int(fc_lc);
			PGPROC	   *fc_proc = BackendPidGetProc(fc_pid);

			if (fc_proc != NULL)
			{
				/*
				 * 如果我们有 setsid()，则向后端的整个进程
				 * 组发送信号
				 */
#ifdef HAVE_SETSID
				(void) kill(-fc_pid, SIGTERM);
#else
				(void) kill(fc_pid, SIGTERM);
#endif
			}
		}
	}
}

/*
 * ProcArraySetReplicationSlotXmin
 *
 * 安装未来的 xmin 地平线计算限制，以防止真空
 * 和 HOT 修剪移除仍由具有复制插槽的客户端所需的受影响行。
 */
void ProcArraySetReplicationSlotXmin(TransactionId fc_xmin, TransactionId fc_catalog_xmin,
								bool fc_already_locked)
{
	Assert(!fc_already_locked || LWLockHeldByMe(ProcArrayLock));

	if (!fc_already_locked)
		LWLockAcquire(ProcArrayLock, LW_EXCLUSIVE);

	procArray->replication_slot_xmin = fc_xmin;
	procArray->replication_slot_catalog_xmin = fc_catalog_xmin;

	if (!fc_already_locked)
		LWLockRelease(ProcArrayLock);

	elog(DEBUG1, "xmin required by slots: data %u, catalog %u",
		 fc_xmin, fc_catalog_xmin);
}

/*
 * ProcArrayGetReplicationSlotXmin
 *
 * 返回当前插槽的 xmin 限制。这对于能够移除
 * 比这些限制更旧的数据是有用的。
 */
void ProcArrayGetReplicationSlotXmin(TransactionId *fc_xmin,
								TransactionId *fc_catalog_xmin)
{
	LWLockAcquire(ProcArrayLock, LW_SHARED);

	if (fc_xmin != NULL)
		*fc_xmin = procArray->replication_slot_xmin;

	if (fc_catalog_xmin != NULL)
		*fc_catalog_xmin = procArray->replication_slot_catalog_xmin;

	LWLockRelease(ProcArrayLock);
}

/*
 * XidCacheRemoveRunningXids
 *
 * 从我后端的已知运行子事务列表中移除一堆 TransactionIds。
 * 指定的 xid 和 xids[] 数组中的那些（长度为 nxids）都将从
 * subxids 缓存中移除。latestXid 必须是该组中的最新 XID。
 */
void XidCacheRemoveRunningXids(TransactionId fc_xid,
						  int fc_nxids, const TransactionId *fc_xids,
						  TransactionId fc_latestXid)
{
	int			fc_i,
				fc_j;
	XidCacheStatus *fc_mysubxidstat;

	Assert(TransactionIdIsValid(fc_xid));

	/*
	 * 我们必须独占持有 ProcArrayLock 以从 PGPROC 数组中移除事务。
	 * （请参见 src/backend/access/transam/README。）这可能会放宽，因为我们知道
	 * 该例程仅用于中止子事务，但在更仔细的分析之前，我们最好
	 * 保持保守。
	 *
	 * 请注意，我们不必在 GetNewTransactionId() 上小心
	 * 我们自己的读取的内存排序——只有这个进程可以修改
	 * MyProc/ProcGlobal->xids[] 的相关字段。但我们必须
	 * 小心我们自己的写入保持良好排序。
	 */
	LWLockAcquire(ProcArrayLock, LW_EXCLUSIVE);

	fc_mysubxidstat = &ProcGlobal->subxidStates[MyProc->pgxactoff];

	/*
	 * 在正常情况下，xid 和 xids[] 将按递增顺序排列，
	 * 子 xids 中的条目也是如此。向后扫描以避免 O(N^2)
	 * 行为，在移除大量 xids 时。
	 */
	for (fc_i = fc_nxids - 1; fc_i >= 0; fc_i--)
	{
		TransactionId fc_anxid = fc_xids[fc_i];

		for (fc_j = MyProc->subxidStatus.count - 1; fc_j >= 0; fc_j--)
		{
			if (TransactionIdEquals(MyProc->subxids.xids[fc_j], fc_anxid))
			{
				MyProc->subxids.xids[fc_j] = MyProc->subxids.xids[MyProc->subxidStatus.count - 1];
				pg_write_barrier();
				fc_mysubxidstat->count--;
				MyProc->subxidStatus.count--;
				break;
			}
		}

		
/*
		 * 通常情况下，我们应该找到它，除非缓存溢出。然而，这个例程也可能在AbortSubTransaction发生错误时被多次调用同一个子事务。因此，不要使用Assert，发出调试警告。
		 */
		if (fc_j < 0 && !MyProc->subxidStatus.overflowed)
			elog(WARNING, "did not find subXID %u in MyProc", fc_anxid);
	}

	for (fc_j = MyProc->subxidStatus.count - 1; fc_j >= 0; fc_j--)
	{
		if (TransactionIdEquals(MyProc->subxids.xids[fc_j], fc_xid))
		{
			MyProc->subxids.xids[fc_j] = MyProc->subxids.xids[MyProc->subxidStatus.count - 1];
			pg_write_barrier();
			fc_mysubxidstat->count--;
			MyProc->subxidStatus.count--;
			break;
		}
	}
	/* 通常情况下，我们应该找到它，除非缓存溢出 */
	if (fc_j < 0 && !MyProc->subxidStatus.overflowed)
		elog(WARNING, "did not find subXID %u in MyProc", fc_xid);

	/* 在持有锁的同时也要推进全局 latestCompletedXid */
	fc_MaintainLatestCompletedXid(fc_latestXid);

	/* ... 和xactCompletionCount */
	ShmemVariableCache->xactCompletionCount++;

	LWLockRelease(ProcArrayLock);
}

#ifdef XIDCACHE_DEBUG

/*
 * 打印有关XID缓存有效性的统计信息
 */
static void fc_DisplayXidCache(void)
{
	fprintf(stderr,
			"XidCache: xmin: %ld, known: %ld, myxact: %ld, latest: %ld, mainxid: %ld, childxid: %ld, knownassigned: %ld, nooflo: %ld, slow: %ld\n",
			xc_by_recent_xmin,
			xc_by_known_xact,
			xc_by_my_xact,
			xc_by_latest_xid,
			xc_by_main_xid,
			xc_by_child_xid,
			xc_by_known_assigned,
			xc_no_overflow,
			xc_slow_answer);
}
#endif							/* XIDCACHE_DEBUG */

/*
 * 如果rel != NULL，返回适合关系的测试状态，否则返回适用于所有关系的状态。后者可能将XIDs视为尚未对所有人可见，而特定关系的状态则已将其视为对所有人可见。
 *
 * 这需要在快照处于活动或注册状态时调用，否则会存在绕回和其他危险。
 *
 * 有关详细信息，请参见GlobalVisState的注释。
 */
GlobalVisState *
GlobalVisTestFor(Relation fc_rel)
{
	GlobalVisState *fc_state = NULL;

	/* XXX: 我们应该断言已经推送或注册了一个快照 */
	Assert(RecentXmin);

	switch (fc_GlobalVisHorizonKindForRel(fc_rel))
	{
		case VISHORIZON_SHARED:
			fc_state = &GlobalVisSharedRels;
			break;
		case VISHORIZON_CATALOG:
			fc_state = &GlobalVisCatalogRels;
			break;
		case VISHORIZON_DATA:
			fc_state = &GlobalVisDataRels;
			break;
		case VISHORIZON_TEMP:
			fc_state = &GlobalVisTempRels;
			break;
	}

	Assert(FullTransactionIdIsValid(fc_state->definitely_needed) &&
		   FullTransactionIdIsValid(fc_state->maybe_needed));

	return fc_state;
}

/*
 * 如果更新准确的maybe_needed边界是值得的，则返回true。
 *
 * 由于确定xmin边界的代价相对较高，我们不想在低概率情况下反复进行此操作。
 *
 * 当前的启发式方法是，仅当RecentXmin自上次更新以来发生变化时才更新。如果当前运行的最旧事务尚未完成，重新计算边界的可能性不大。
 */
static bool fc_GlobalVisTestShouldUpdate(GlobalVisState *fc_state)
{
	/* 尚未更新 */
	if (!TransactionIdIsValid(ComputeXidHorizonsResultLastXmin))
		return true;

	/*
	 * 如果maybe_needed/definitely_needed边界相同，刷新边界可能无益。
	 */
	if (FullTransactionIdFollowsOrEquals(fc_state->maybe_needed,
										 fc_state->definitely_needed))
		return false;

	/* 最后构建的快照是否具有不同的xmin？ */
	return RecentXmin != ComputeXidHorizonsResultLastXmin;
}

static void fc_GlobalVisUpdateApply(ComputeXidHorizonsResult *fc_horizons)
{
	GlobalVisSharedRels.maybe_needed =
		fc_FullXidRelativeTo(fc_horizons->latest_completed,
						  fc_horizons->shared_oldest_nonremovable);
	GlobalVisCatalogRels.maybe_needed =
		fc_FullXidRelativeTo(fc_horizons->latest_completed,
						  fc_horizons->catalog_oldest_nonremovable);
	GlobalVisDataRels.maybe_needed =
		fc_FullXidRelativeTo(fc_horizons->latest_completed,
						  fc_horizons->data_oldest_nonremovable);
	GlobalVisTempRels.maybe_needed =
		fc_FullXidRelativeTo(fc_horizons->latest_completed,
						  fc_horizons->temp_oldest_nonremovable);

	/*
	 * 在较长时间运行的事务中，之前需要视为运行的事务可能不再存在。因此将definitely_needed更新为不早于maybe_needed。
	 */
	GlobalVisSharedRels.definitely_needed =
		FullTransactionIdNewer(GlobalVisSharedRels.maybe_needed,
							   GlobalVisSharedRels.definitely_needed);
	GlobalVisCatalogRels.definitely_needed =
		FullTransactionIdNewer(GlobalVisCatalogRels.maybe_needed,
							   GlobalVisCatalogRels.definitely_needed);
	GlobalVisDataRels.definitely_needed =
		FullTransactionIdNewer(GlobalVisDataRels.maybe_needed,
							   GlobalVisDataRels.definitely_needed);
	GlobalVisTempRels.definitely_needed = GlobalVisTempRels.maybe_needed;

	ComputeXidHorizonsResultLastXmin = RecentXmin;
}

/*
 * 使用ComputeXidHorizons()更新GlobalVis{Shared,Catalog, Data}Rels中的边界。
 */
static void fc_GlobalVisUpdate(void)
{
	ComputeXidHorizonsResult fc_horizons;

	/* 作为副作用更新边界 */
	fc_ComputeXidHorizons(&fc_horizons);
}

/*
 * 如果没有快照仍然认为 fxid 在运行，则返回 true。
 *
 * 传递的状态需要为 fxid 所在的关系初始化（NULL 也是可以的），否则结果可能不正确。
 *
 * 有关详细信息，请参见 GlobalVisState 的注释。
 */
bool GlobalVisTestIsRemovableFullXid(GlobalVisState *fc_state,
								FullTransactionId fc_fxid)
{
	/*
	 * 如果 fxid 比 maybe_needed 界限更旧，则它肯定对
	 * 所有人可见。
	 */
	if (FullTransactionIdPrecedes(fc_fxid, fc_state->maybe_needed))
		return true;

	/*
	 * 如果 fxid >= definitely_needed 界限，很可能仍然会被
	 * 认为在运行。
	 */
	if (FullTransactionIdFollowsOrEquals(fc_fxid, fc_state->definitely_needed))
		return false;

	/*
	 * fxid 在 maybe_needed 和 definitely_needed 之间，即可能
	 * 存在或不存在一个快照将 fxid 视为运行。如果有意义，
	 * 更新边界并重新检查。
	 */
	if (fc_GlobalVisTestShouldUpdate(fc_state))
	{
		fc_GlobalVisUpdate();

		Assert(FullTransactionIdPrecedes(fc_fxid, fc_state->definitely_needed));

		return FullTransactionIdPrecedes(fc_fxid, fc_state->maybe_needed);
	}
	else
		return false;
}

/*
 * 针对 32 位 xids 的 GlobalVisTestIsRemovableFullXid() 的包装器。
 *
 * 关键是这只能对来自保护 xids 防止环绕的源调用（例如
 * 来自一个表，因此受 relfrozenxid 保护）。
 */
bool GlobalVisTestIsRemovableXid(GlobalVisState *fc_state, TransactionId fc_xid)
{
	FullTransactionId fc_fxid;

	/*
	 * 将 32 位参数转换为 FullTransactionId。我们可以安全地
	 * 这样做，因为我们知道 xid 至少必须在
	 * [oldestXid, nextFullXid) 之间，即在 xid 的 20 亿范围内。为了避免
	 * 锁定以确定任一项，我们可以与
	 * state->definitely_needed 进行比较，该值是基于当前快照构建时的那些值。
	 */
	fc_fxid = fc_FullXidRelativeTo(fc_state->definitely_needed, fc_xid);

	return GlobalVisTestIsRemovableFullXid(fc_state, fc_fxid);
}

/*
 * 返回所有事务不再被视为运行的 FullTransactionId。
 *
 * 注意：这比使用 GlobalVisTestIsRemovableFullXid 测试效率低，
 * 因为它可能需要构建一个准确的截止点，即使在所有与截止点比较的 XIDs
 * 均在 [maybe_needed, definitely_needed) 之外的情况下。
 */
FullTransactionId GlobalVisTestNonRemovableFullHorizon(GlobalVisState *fc_state)
{
	/* 如果尚未完成，则获取准确的地平线 */
	if (fc_GlobalVisTestShouldUpdate(fc_state))
		fc_GlobalVisUpdate();

	return fc_state->maybe_needed;
}

/* GlobalVisTestNonRemovableFullHorizon 的便利包装 */
TransactionId GlobalVisTestNonRemovableHorizon(GlobalVisState *fc_state)
{
	FullTransactionId fc_cutoff;

	fc_cutoff = GlobalVisTestNonRemovableFullHorizon(fc_state);

	return XidFromFullTransactionId(fc_cutoff);
}

/*
 * GlobalVisTestFor() 和 GlobalVisTestIsRemovableFullXid() 的便利包装，
 * 请参阅它们的注释。
 */
bool GlobalVisCheckRemovableFullXid(Relation fc_rel, FullTransactionId fc_fxid)
{
	GlobalVisState *fc_state;

	fc_state = GlobalVisTestFor(fc_rel);

	return GlobalVisTestIsRemovableFullXid(fc_state, fc_fxid);
}

/*
 * GlobalVisTestFor() 和 GlobalVisTestIsRemovableXid() 的便利包装，
 * 请参阅它们的注释。
 */
bool GlobalVisCheckRemovableXid(Relation fc_rel, TransactionId fc_xid)
{
	GlobalVisState *fc_state;

	fc_state = GlobalVisTestFor(fc_rel);

	return GlobalVisTestIsRemovableXid(fc_state, fc_xid);
}

/*
 * 通过 retreat_by 安全地减少 *xid，把结果存储在 *xid 中。
 *
 * 必须小心防止 *xid 在 0 代期间退回低于
 * FirstNormalTransactionId。这对于防止生成
 * 无法转换为 FullTransactionId 而不发生环绕的 xid 至关重要。
 *
 * 如果 retreat_by 会导致一个太旧的 xid，将返回 FirstNormalTransactionId。
 */
static void fc_TransactionIdRetreatSafely(TransactionId *fc_xid, int fc_retreat_by, FullTransactionId fc_rel)
{
	TransactionId fc_original_xid = *fc_xid;
	FullTransactionId fc_fxid;
	uint64		fc_fxid_i;

	Assert(TransactionIdIsNormal(fc_original_xid));
	Assert(fc_retreat_by >= 0);	/* 相关的 GUCs 存储为整数 */
	AssertTransactionIdInAllowableRange(fc_original_xid);

	if (fc_retreat_by == 0)
		return;

	fc_fxid = fc_FullXidRelativeTo(fc_rel, fc_original_xid);
	fc_fxid_i = U64FromFullTransactionId(fc_fxid);

	if ((fc_fxid_i - FirstNormalTransactionId) <= fc_retreat_by)
		*fc_xid = FirstNormalTransactionId;
	else
	{
		*fc_xid = TransactionIdRetreatedBy(fc_original_xid, fc_retreat_by);
		Assert(TransactionIdIsNormal(*fc_xid));
		Assert(NormalTransactionIdPrecedes(*fc_xid, fc_original_xid));
	}
}

/*
 * 将 32 位事务 ID 转换为 64 位事务 ID，假设它
 * 在 XidFromFullTransactionId(rel) 的 MaxTransactionId / 2 之内。
 *
 * 在使用此函数时要非常小心。仅在确保 xid 在
 * rel 的 MaxTransactionId / 2 xids 内时才能安全使用。
 * 例如，如果调用者保证后台持有快照并且 xid 来自一个表（
 * 其中 vacuum/freezing 确保 xid 必须在该范围内），
 * 或者如果 xid 来自 procarray 并以这种方式防止 xid 环绕。
 */
static inline FullTransactionId
fc_FullXidRelativeTo(FullTransactionId fc_rel, TransactionId fc_xid)
{
	TransactionId fc_rel_xid = XidFromFullTransactionId(fc_rel);

	Assert(TransactionIdIsValid(fc_xid));
	Assert(TransactionIdIsValid(fc_rel_xid));

	/* 不保证发现问题，但可能会捕获错误 */
	AssertTransactionIdInAllowableRange(fc_xid);

	return FullTransactionIdFromU64(U64FromFullTransactionId(fc_rel)
									+ (int32) (fc_xid - fc_rel_xid));
}


/* ----------------------------------------------
 *		KnownAssignedTransactionIds 子模块
 * ----------------------------------------------
 */


/*
 * 在热备份模式下，我们保持一份当前在WAL中运行的主服务器事务（或曾经运行的事务）的列表。这些XID必须被视为由备用事务运行，即使它们没有出现在备用服务器的PGPROC数组中。
 *
 * 我们记录所有已知已分配的XID。这包括在WAL记录中看到的所有XID，以及我们推测已分配的所有未观察到的XID。我们可以推测未观察到的XID的存在，因为我们知道XID是按顺序分配的，没有空缺。KnownAssignedXids列表随着新XID的观察或推测而扩展，并在事务完成记录到达时收缩。
 *
 * 在热备份期间，我们不会过于担心顶级XID和子事务XID之间的区别。我们将两者一起存储在KnownAssignedXids列表中。在后端，这会复制到GetSnapshotData()中的快照中，利用了XidInMVCCSnapshot()对这种区别不在意的事实。子事务XID实际上被视为顶级XID，并且在典型情况下pg_subtrans链接是*不*维护的（这不会影响可见性）。
 *
 * 我们在KnownAssignedXids和快照中有空间容纳maxProcs * (1 + PGPROC_MAX_CACHED_SUBXIDS)个XID，因此每个主事务必须在每个PGPROC_MAX_CACHED_SUBXIDS中至少通过WAL XLOG_XACT_ASSIGNMENT记录报告其子事务XID。当我们收到这些记录之一时，我们将子XID标记为pg_subtrans中顶级XID的子项，然后从KnownAssignedXids中将其移除。这防止了KnownAssignedXids和快照的溢出，但代价是这些子XID的状态检查将在TransactionIdIsInProgress()中走一个较慢的路径。这意味着KnownAssignedXids对于子XID不一定是完整的，尽管它对于顶级XID应该是完整的；这种情况与正常运行中的PGPROC条目是相同的。
 *
 * 当我们从KnownAssignedXids中丢弃子XID时，我们需要跟踪这一点，类似于跟踪PGPROC的subxids数组的溢出。我们通过记住lastOverflowedXid，即最后丢弃的子XID，来做到这一点。只要该值在有趣的XID范围内，我们就必须假设快照中缺少子XID。（注意，子XID溢出发生在主服务器当第65个子XID到达时，而在备用服务器当第64个子XID到达时——这不是错误。）
 *
 * 如果主服务器上的后端在能写入中止记录之前以某种方式消失，那么我们就只会将这些XID留在KnownAssignedXids中。它们实际上已被中止，但我们认为它们正在运行；这种区别是无关紧要的，因为无论如何任何由事务完成的更改对于备用中的后端都是不可见的。我们在接收到XLOG_RUNNING_XACTS时修剪KnownAssignedXids，以防止由于这些死XID引起的数组溢出。
 */

/*
 * RecordKnownAssignedTransactionIds
 *		记录给定的XID到KnownAssignedXids中，以及任何先前未观察到的XID。
 *
 * RecordKnownAssignedTransactionIds()应该为与事务相关的*每个* WAL记录运行。在执行StartupCLOG()等之后，必须为每条记录调用，因为我们必须ExtendCLOG()等。
 *
 * 在恢复期间被调用，类似于并代替GetNewTransactionId()
 */
void RecordKnownAssignedTransactionIds(TransactionId fc_xid)
{
	Assert(standbyState >= STANDBY_INITIALIZED);
	Assert(TransactionIdIsValid(fc_xid));
	Assert(TransactionIdIsValid(latestObservedXid));

	elog(trace_recovery(DEBUG4), "record known xact %u latestObservedXid %u",
		 fc_xid, latestObservedXid);

	/*
	 * 当新的观察到的xid到达时，通常情况下它*不是*序列中的下一个xid。当这种情况发生时，我们必须将中间的xids也视为正在运行。
	 */
	if (TransactionIdFollows(fc_xid, latestObservedXid))
	{
		TransactionId fc_next_expected_xid;

		/*
		 * 像在正常操作期间的GetNewTransactionId()中那样，通过单个扩展步骤扩展subtrans。注意我们不需要扩展clog，因为它的扩展已记录在WAL中。
		 *
		 * 无论备用状态如何，这部分都必须完成，因为我们立即开始将子事务分配给其顶级事务。
		 */
		fc_next_expected_xid = latestObservedXid;
		while (TransactionIdPrecedes(fc_next_expected_xid, fc_xid))
		{
			TransactionIdAdvance(fc_next_expected_xid);
			ExtendSUBTRANS(fc_next_expected_xid);
		}
		Assert(fc_next_expected_xid == fc_xid);

		/*
		 * 如果KnownAssignedXids机制尚未启动，那么没有更多事情可做，因为我们尚未跟踪已分配的xids。
		 */
		if (standbyState <= STANDBY_INITIALIZED)
		{
			latestObservedXid = fc_xid;
			return;
		}

		/*
		 * 将(latestObservedXid, xid]添加到KnownAssignedXids数组中。
		 */
		fc_next_expected_xid = latestObservedXid;
		TransactionIdAdvance(fc_next_expected_xid);
		fc_KnownAssignedXidsAdd(fc_next_expected_xid, fc_xid, false);

		/*
		 * 现在我们可以推进latestObservedXid
		 */
		latestObservedXid = fc_xid;

		/* ShmemVariableCache->nextXid必须超过任何观察到的xid */
		AdvanceNextFullTransactionIdPastXid(latestObservedXid);
	}
}

/*
 * ExpireTreeKnownAssignedTransactionIds
 *		从KnownAssignedXids中移除给定的XID。
 *
 * 在恢复期间被调用，类似于并代替ProcArrayEndTransaction()
 */
void ExpireTreeKnownAssignedTransactionIds(TransactionId fc_xid, int fc_nsubxids,
									  TransactionId *fc_subxids, TransactionId fc_max_xid)
{
	Assert(standbyState >= STANDBY_INITIALIZED);

	/*
	 * 使用与事务提交相同的锁定
	 */
	LWLockAcquire(ProcArrayLock, LW_EXCLUSIVE);

	fc_KnownAssignedXidsRemoveTree(fc_xid, fc_nsubxids, fc_subxids);

	/* 如同在ProcArrayEndTransaction中，推进latestCompletedXid */
	fc_MaintainLatestCompletedXidRecovery(fc_max_xid);

	/* ... 和xactCompletionCount */
	ShmemVariableCache->xactCompletionCount++;

	LWLockRelease(ProcArrayLock);
}

/*
 * ExpireAllKnownAssignedTransactionIds
 *		从KnownAssignedXids中移除所有条目并重置lastOverflowedXid。
 */
void ExpireAllKnownAssignedTransactionIds(void)
{
	LWLockAcquire(ProcArrayLock, LW_EXCLUSIVE);
	fc_KnownAssignedXidsRemovePreceding(InvalidTransactionId);

	
	procArray->lastOverflowedXid = InvalidTransactionId;
	LWLockRelease(ProcArrayLock);
}

/***********************************************
 * ExpireOldKnownAssignedTransactionIds
 *		移除给定XID之前的KnownAssignedXids条目，并
 *		可能重置lastOverflowedXid。
 ***********************************************/
void ExpireOldKnownAssignedTransactionIds(TransactionId fc_xid)
{
	LWLockAcquire(ProcArrayLock, LW_EXCLUSIVE);

	/***********************************************
	 * 如果我们知道所有可能正在运行的事务已经结束，
	 * 则重置lastOverflowedXid。否则可能导致不正确的
	 * lastOverflowedXid值，从而使额外的快照被标记为
	 * suboverflowed。
 ***********************************************/
	if (TransactionIdPrecedes(procArray->lastOverflowedXid, fc_xid))
		procArray->lastOverflowedXid = InvalidTransactionId;
	fc_KnownAssignedXidsRemovePreceding(fc_xid);
	LWLockRelease(ProcArrayLock);
}

/***********************************************
 * KnownAssignedTransactionIdsIdleMaintenance
 *		当启动进程即将空闲时，机会性地进行维护工作。
 ***********************************************/
void KnownAssignedTransactionIdsIdleMaintenance(void)
{
	fc_KnownAssignedXidsCompress(KAX_STARTUP_PROCESS_IDLE, false);
}



/*
 * 操作 KnownAssignedXids 的私有模块函数
 *
 * KnownAssignedXids 数据结构有 5 种主要用途：
 *
 *	* 后台进程获取快照 - 所有有效的 XID 需要被复制出来
 *	* 后台进程确定特定 XID 的存在
 *	* 启动进程添加新的已知分配 XID
 *	* 启动进程在事务结束时移除特定 XID
 *	* 启动进程在特殊 WAL 记录到达时修剪数组
 *
 * 众所周知，该数据结构在热备用期间是一个热点，因此我们
 * 付出了一些努力使这些操作尽可能高效和并发。
 *
 * XID 以排序顺序存储在数组中——确切地说是 TransactionIdPrecedes
 * 顺序——以便允许对特定 XID 进行二分搜索。注意：
 * 一般而言，TransactionIdPrecedes 不会提供总的顺序，但
 * 我们知道在任何时刻存在的条目不应扩展到 XID 空间的
 * 足够大的一部分而发生环绕（主服务器会因为担心 XID 环绕
 * 很早前就关闭）。所以使用 TransactionIdPrecedes 作为
 * 二分搜索比较器是可以的。
 *
 * 在插入时维护有序性是便宜的，因为新的已知 XID 总是
 * 按 XID 顺序报告；我们只需将它们附加到右侧。
 *
 * 为了保持单个删除的低开销，我们需要允许数组中有间隙。
 * 这是通过使用并行布尔数组 KnownAssignedXidsValid[] 将数组元素
 * 标记为有效或无效来实现的。删除通过将 KnownAssignedXidsValid[i]
 * 设置为 false 来完成，而 *不* 清除 XID 条目本身。
 * 这保留了 XID 条目排序的属性，因此我们可以轻松地进行
 * 二分搜索。我们会定期压缩未使用的条目；这比在每次
 * 删除时立即压缩数组要便宜得多。
 *
 * KnownAssignedXids[] 和 KnownAssignedXidsValid[] 中实际有效的
 * 项目是索引 tail <= i < head；该下标范围之外的项目
 * 内容未定义。当 head 达到数组的末尾时，我们会强制压缩
 * 未使用的条目，而不是环绕，因为允许环绕会大大复杂化
 * 搜索逻辑。我们维护一个显式的尾指针，以便可以在
 * 不立即移动数组内容的情况下修剪旧的 XID。在大多数
 * 情况下，任何时刻数组中只有一小部分包含有效条目。
 *
 * 尽管只有启动进程能够更改 KnownAssignedXids 数据结构，
 * 我们仍然需要锁定，以便备用后台进程不会观察到无效的
 * 中间状态。惯例是后台进程必须持有共享的 ProcArrayLock
 * 来检查数组。为了从数组中移除 XID，启动进程必须独占
 * 持有 ProcArrayLock，出于通常的事务原因（比较正常
 * 运行期间事务的提交/中止）。同样，压缩数组中未使用的
 * 条目也需要独占锁。为了将 XID 添加到数组中，我们只需
 * 将它们插入到头指针右侧的槽中，然后推进头指针。除了
 * 在内存顺序较弱的机器上，我们需要确保其他处理器
 * 在看到头指针变化之前看到数组元素变化，这通常不需要
 * 锁。我们通过使用自旋锁来保护头/尾指针的读取和写入
 * 来处理这一点。（如果我们创建合适的内存访问屏障原语
 * 并使用那些，我们可以不使用自旋锁。）
 * 除非调用方独占持有 ProcArrayLock，否则必须获取自旋锁
 * 以读取或写入头/尾指针。
 *
 * 算法分析：
 *
 * 如果我们最多有 M 个槽，其中 N 个 XID 当前分布在 S 个元素中，
 * 那么我们始终有 N <= S <= M。
 *
 *	* 添加一个新的 XID 是 O(1) 并且需要很少的锁（除非必须压缩）
 *	* 压缩数组是 O(S) 且需要独占锁
 *	* 移除一个 XID 是 O(logS) 并且需要独占锁
 *	* 获取快照是 O(S) 且需要共享锁
 *	* 检查一个 XID 是 O(logS) 并且需要共享锁
 *
 * 相比之下，使用哈希表来管理 KnownAssignedXids 意味着
 * 获取快照的复杂度将是 O(M)。如果我们能维持 S << M ，
 * 那么排序数组技术将提供显著更快的快照。
 * 如果我们尝试将 S 保持得太小，那么我们将花费过多的时间
 * 进行压缩，因此对于任何工作负载混合都有一个最佳点。
 * 我们使用启发式方法来决定何时压缩数组，尽管修剪也有助于
 * 减少压缩的频率。启发式需要我们跟踪数组中当前有效的
 * XID 数量（N）。除非在特殊情况下，当 S >= 2N 时我们
 * 会进行压缩。将 S 限制在 2N 也反过来将获取快照的时间
 * 确定为 O(N)，这本来就是必须的。
 */



/*
 * 压缩 KnownAssignedXids，通过将有效数据向下移动到数组的起始位置，去除任何间隙。
 *
 * 如果“reason”为 KAX_NO_SPACE，则强制进行压缩，否则仅在启发式方法表明这是一个好时机时进行。
 *
 * 压缩需要以独占模式保持 ProcArrayLock。
 * 调用者如果已经持有锁，则必须传递 haveLock = true。
 */
static void fc_KnownAssignedXidsCompress(KAXCompressReason fc_reason, bool fc_haveLock)
{
	ProcArrayStruct *fc_pArray = procArray;
	int			fc_head,
				fc_tail,
				fc_nelements;
	int			fc_compress_index;
	int			fc_i;

	/* 压缩启发式方法的计数器 */
	static unsigned int fc_transactionEndsCounter;
	static TimestampTz fc_lastCompressTs;

	/* 调节常数 */
#define KAX_COMPRESS_FREQUENCY 128	/* 在事务中 */
#define KAX_COMPRESS_IDLE_INTERVAL 1000 /* in ms */

	/*
	 * 由于只有启动流程修改头/尾指针，因此这里不需要锁来读取它们。
	 */
	fc_head = fc_pArray->headKnownAssignedXids;
	fc_tail = fc_pArray->tailKnownAssignedXids;
	fc_nelements = fc_head - fc_tail;

	/*
	 * 如果我们可以选择是否进行压缩，则使用启发式方法来避免
	 * 过于频繁或不够频繁地压缩。这里的“压缩”仅仅意味着将值
	 * 移动到数组的开头，因此它不像典型的数据压缩算法那么复杂
	 * 或昂贵。
	 */
	if (fc_nelements == fc_pArray->numKnownAssignedXids)
	{
		/*
		 * 当头和尾之间没有间隙时，不必进行压缩，除了在 KAX_NO_SPACE
		 * 的情况下我们必须进行压缩以在头后创建一些空间。
		 */
		if (fc_reason != KAX_NO_SPACE)
			return;
	}
	else if (fc_reason == KAX_TRANSACTION_END)
	{
		/*
		 * 考虑每多少次提交进行一次压缩。频率
		 * 由基准测试决定。
		 */
		if ((fc_transactionEndsCounter++) % KAX_COMPRESS_FREQUENCY != 0)
			return;

		/*
		 * 此外，仅当数组的使用部分少于 50% 满时进行压缩
		 * （见上面评论）。
		 */
		if (fc_nelements < 2 * fc_pArray->numKnownAssignedXids)
			return;
	}
	else if (fc_reason == KAX_STARTUP_PROCESS_IDLE)
	{
		/*
		 * 我们即将因缺少新的 WAL 而闲置，所以我们可以进行压缩。
		 * 但不要太频繁，以避免与读取者之间的 ProcArray 锁争用。
		 */
		if (fc_lastCompressTs != 0)
		{
			TimestampTz fc_compress_after;

			fc_compress_after = TimestampTzPlusMilliseconds(fc_lastCompressTs,
														 KAX_COMPRESS_IDLE_INTERVAL);
			if (GetCurrentTimestamp() < fc_compress_after)
				return;
		}
	}

	/* 需要压缩，因此如果我们没有锁，则获取锁。 */
	if (!fc_haveLock)
		LWLockAcquire(ProcArrayLock, LW_EXCLUSIVE);

	/*
	 * 我们通过从尾部到头部读取有效值来压缩数组，
	 * 将数据重新对齐到第 0 个元素。
	 */
	fc_compress_index = 0;
	for (fc_i = fc_tail; fc_i < fc_head; fc_i++)
	{
		if (KnownAssignedXidsValid[fc_i])
		{
			KnownAssignedXids[fc_compress_index] = KnownAssignedXids[fc_i];
			KnownAssignedXidsValid[fc_compress_index] = true;
			fc_compress_index++;
		}
	}
	Assert(fc_compress_index == fc_pArray->numKnownAssignedXids);

	fc_pArray->tailKnownAssignedXids = 0;
	fc_pArray->headKnownAssignedXids = fc_compress_index;

	if (!fc_haveLock)
		LWLockRelease(ProcArrayLock);

	/* 更新维护的时间戳。对此不需要保持锁。 */
	fc_lastCompressTs = GetCurrentTimestamp();
}

/*
 * 将 xids 添加到数组头部的 KnownAssignedXids 中。
 *
 * 从 from_xid 到 to_xid（包括）之间的 xids 被添加到数组中。
 *
 * 如果 exclusive_lock 为真，则调用者已经在独占模式下持有
 * ProcArrayLock，因此这里不需要额外的锁定。否则，调用者没有
 * 锁，因此我们需要确保保持足够的互锁以防止并发读取。
 * （只有启动进程会调用此函数，因此无需担心并发写入。）
 */
static void fc_KnownAssignedXidsAdd(TransactionId fc_from_xid, TransactionId fc_to_xid,
					 bool fc_exclusive_lock)
{
	ProcArrayStruct *fc_pArray = procArray;
	TransactionId fc_next_xid;
	int			fc_head,
				fc_tail;
	int			fc_nxids;
	int			fc_i;

	Assert(TransactionIdPrecedesOrEquals(fc_from_xid, fc_to_xid));

	/*
	 * 计算我们需要多少个数组槽。通常这很便宜；
	 * 在 XIDs 跨越包装点的特殊情况下，我们将以困难的方式进行。
	 */
	if (fc_to_xid >= fc_from_xid)
		fc_nxids = fc_to_xid - fc_from_xid + 1;
	else
	{
		fc_nxids = 1;
		fc_next_xid = fc_from_xid;
		while (TransactionIdPrecedes(fc_next_xid, fc_to_xid))
		{
			fc_nxids++;
			TransactionIdAdvance(fc_next_xid);
		}
	}

	/*
	 * 由于只有启动流程修改头/尾指针，因此这里不需要锁来读取它们。
	 */
	fc_head = fc_pArray->headKnownAssignedXids;
	fc_tail = fc_pArray->tailKnownAssignedXids;

	Assert(fc_head >= 0 && fc_head <= fc_pArray->maxKnownAssignedXids);
	Assert(fc_tail >= 0 && fc_tail < fc_pArray->maxKnownAssignedXids);

	/*
	 * 验证插入是否按 TransactionId 顺序进行。
	 * 注意，即使最后存在的元素被标记为无效，它仍然必须具有
	 * 正确排序的 XID 值。
	 */
	if (fc_head > fc_tail &&
		TransactionIdFollowsOrEquals(KnownAssignedXids[fc_head - 1], fc_from_xid))
	{
		fc_KnownAssignedXidsDisplay(LOG);
		elog(ERROR, "out-of-order XID insertion in KnownAssignedXids");
	}

	/*
	 * 如果我们的 xids 不能适合剩余空间，则压缩出空闲空间
	 */
	if (fc_head + fc_nxids > fc_pArray->maxKnownAssignedXids)
	{
		fc_KnownAssignedXidsCompress(KAX_NO_SPACE, fc_exclusive_lock);

		fc_head = fc_pArray->headKnownAssignedXids;
		/* 注意：我们不再关心尾指针 */

		/*
		 * 如果它仍然不适合，则我们内存不足
		 */
		if (fc_head + fc_nxids > fc_pArray->maxKnownAssignedXids)
			elog(ERROR, "too many KnownAssignedXids");
	}

	/* 现在我们可以将 xids 插入从头开始的空间 */
	fc_next_xid = fc_from_xid;
	for (fc_i = 0; fc_i < fc_nxids; fc_i++)
	{
		KnownAssignedXids[fc_head] = fc_next_xid;
		KnownAssignedXidsValid[fc_head] = true;
		TransactionIdAdvance(fc_next_xid);
		fc_head++;
	}

	/* 调整有效条目数量 */
	fc_pArray->numKnownAssignedXids += fc_nxids;

	/*
	 * 现在更新头指针。我们使用自旋锁来保护此指针，
	 * 不是因为更新可能是非原子性的，而是为了确保其他处理器
	 * 在看到头指针变化之前，看到上述数组更新。
	 *
	 * 如果我们独占地持有 ProcArrayLock，则不需要获取自旋锁。
	 */
	if (fc_exclusive_lock)
		fc_pArray->headKnownAssignedXids = fc_head;
	else
	{
		SpinLockAcquire(&fc_pArray->known_assigned_xids_lck);
		fc_pArray->headKnownAssignedXids = fc_head;
		SpinLockRelease(&fc_pArray->known_assigned_xids_lck);
	}
}

/*
 * KnownAssignedXidsSearch
 *
 * 在 KnownAssignedXids 中搜索特定的 xid，并可选择删除它。
 * 如果找到则返回 true，如果未找到则返回 false。
 *
 * 调用者必须以共享或独占模式持有 ProcArrayLock。
 * 当 remove = true 时，必须持有独占锁。
 */
static bool fc_KnownAssignedXidsSearch(TransactionId fc_xid, bool fc_remove)
{
	ProcArrayStruct *fc_pArray = procArray;
	int			fc_first,
				fc_last;
	int			fc_head;
	int			fc_tail;
	int			fc_result_index = -1;

	if (fc_remove)
	{
		/* 我们独占地持有 ProcArrayLock，因此不需要自旋锁 */
		fc_tail = fc_pArray->tailKnownAssignedXids;
		fc_head = fc_pArray->headKnownAssignedXids;
	}
	else
	{
		/* 获取自旋锁以确保我们看到最新的数组内容 */
		SpinLockAcquire(&fc_pArray->known_assigned_xids_lck);
		fc_tail = fc_pArray->tailKnownAssignedXids;
		fc_head = fc_pArray->headKnownAssignedXids;
		SpinLockRelease(&fc_pArray->known_assigned_xids_lck);
	}

	/*
	 * 标准二分查找。注意我们可以忽略 KnownAssignedXidsValid
	 * 数组，因为即使无效的条目也将包含排序的 XID。
	 */
	fc_first = fc_tail;
	fc_last = fc_head - 1;
	while (fc_first <= fc_last)
	{
		int			fc_mid_index;
		TransactionId fc_mid_xid;

		fc_mid_index = (fc_first + fc_last) / 2;
		fc_mid_xid = KnownAssignedXids[fc_mid_index];

		if (fc_xid == fc_mid_xid)
		{
			fc_result_index = fc_mid_index;
			break;
		}
		else if (TransactionIdPrecedes(fc_xid, fc_mid_xid))
			fc_last = fc_mid_index - 1;
		else
			fc_first = fc_mid_index + 1;
	}

	if (fc_result_index < 0)
		return false;			/* 不在数组中 */

	if (!KnownAssignedXidsValid[fc_result_index])
		return false;			/* 在数组中，但无效 */

	if (fc_remove)
	{
		KnownAssignedXidsValid[fc_result_index] = false;

		fc_pArray->numKnownAssignedXids--;
		Assert(fc_pArray->numKnownAssignedXids >= 0);

		/*
		 * 如果我们要移除尾元素，则将尾指针向前移动到
		 * 任意无效元素。这样可以加快未来的搜索速度。
		 */
		if (fc_result_index == fc_tail)
		{
			fc_tail++;
			while (fc_tail < fc_head && !KnownAssignedXidsValid[fc_tail])
				fc_tail++;
			if (fc_tail >= fc_head)
			{
				/* 数组为空，因此我们可以重置两个指针 */
				fc_pArray->headKnownAssignedXids = 0;
				fc_pArray->tailKnownAssignedXids = 0;
			}
			else
			{
				fc_pArray->tailKnownAssignedXids = fc_tail;
			}
		}
	}

	return true;
}

/*
 * 指定的 XID 是否存在于 KnownAssignedXids[] 中？
 *
 * 调用者必须以共享模式或独占模式持有 ProcArrayLock。
 */
static bool fc_KnownAssignedXidExists(TransactionId fc_xid)
{
	Assert(TransactionIdIsValid(fc_xid));

	return fc_KnownAssignedXidsSearch(fc_xid, false);
}

/*
 * 从 KnownAssignedXids[] 中移除指定的 XID。
 *
 * 调用者必须以独占模式持有 ProcArrayLock。
 */
static void fc_KnownAssignedXidsRemove(TransactionId fc_xid)
{
	Assert(TransactionIdIsValid(fc_xid));

	elog(trace_recovery(DEBUG4), "remove KnownAssignedXid %u", fc_xid);

	/*
	 * 注意：我们不能认为移除一个不存在的 XID 是错误的。
	 * 我们在处理 XLOG_XACT_ASSIGNMENT 时有意移除子事务 ID，
	 * 以避免数组溢出。然后当顶级事务提交或中止时，这些 XID
	 * 将再次被移除。
	 *
	 * 可能可以追踪这些 XID，以将此情况与实际错误区分开，
	 * 但这会很复杂，可能不值得。因此，只需忽略搜索结果。
	 */
	(void) fc_KnownAssignedXidsSearch(fc_xid, true);
}

/*
 * KnownAssignedXidsRemoveTree
 *		移除 xid（如果它不是 InvalidTransactionId）及所有子 xids。
 *
 * 调用者必须以独占模式持有 ProcArrayLock。
 */
static void fc_KnownAssignedXidsRemoveTree(TransactionId fc_xid, int fc_nsubxids,
							TransactionId *fc_subxids)
{
	int			fc_i;

	if (TransactionIdIsValid(fc_xid))
		fc_KnownAssignedXidsRemove(fc_xid);

	for (fc_i = 0; fc_i < fc_nsubxids; fc_i++)
		fc_KnownAssignedXidsRemove(fc_subxids[fc_i]);

	/* 机会性地压缩数组 */
	fc_KnownAssignedXidsCompress(KAX_TRANSACTION_END, true);
}

/*
 * 修剪 KnownAssignedXids 直至，但*不*包括 xid。如果 xid 无效，
 * 则清空整个表。
 *
 * 调用者必须以独占模式持有 ProcArrayLock。
 */
static void fc_KnownAssignedXidsRemovePreceding(TransactionId fc_removeXid)
{
	ProcArrayStruct *fc_pArray = procArray;
	int			fc_count = 0;
	int			fc_head,
				fc_tail,
				fc_i;

	if (!TransactionIdIsValid(fc_removeXid))
	{
		elog(trace_recovery(DEBUG4), "removing all KnownAssignedXids");
		fc_pArray->numKnownAssignedXids = 0;
		fc_pArray->headKnownAssignedXids = fc_pArray->tailKnownAssignedXids = 0;
		return;
	}

	elog(trace_recovery(DEBUG4), "prune KnownAssignedXids to %u", fc_removeXid);

	/*
	 * 从尾部开始标记条目为无效。由于数组是排序的，
	 * 一旦我们到达一个 >= removeXid 的条目就可以停止。
	 */
	fc_tail = fc_pArray->tailKnownAssignedXids;
	fc_head = fc_pArray->headKnownAssignedXids;

	for (fc_i = fc_tail; fc_i < fc_head; fc_i++)
	{
		if (KnownAssignedXidsValid[fc_i])
		{
			TransactionId fc_knownXid = KnownAssignedXids[fc_i];

			if (TransactionIdFollowsOrEquals(fc_knownXid, fc_removeXid))
				break;

			if (!StandbyTransactionIdIsPrepared(fc_knownXid))
			{
				KnownAssignedXidsValid[fc_i] = false;
				fc_count++;
			}
		}
	}

	fc_pArray->numKnownAssignedXids -= fc_count;
	Assert(fc_pArray->numKnownAssignedXids >= 0);

	/*
	 * 如果我们标记了尾部项为无效，则向前移动尾指针。
	 */
	for (fc_i = fc_tail; fc_i < fc_head; fc_i++)
	{
		if (KnownAssignedXidsValid[fc_i])
			break;
	}
	if (fc_i >= fc_head)
	{
		/* 数组为空，因此我们可以重置两个指针 */
		fc_pArray->headKnownAssignedXids = 0;
		fc_pArray->tailKnownAssignedXids = 0;
	}
	else
	{
		fc_pArray->tailKnownAssignedXids = fc_i;
	}

	/* 机会性地压缩数组 */
	fc_KnownAssignedXidsCompress(KAX_PRUNE, true);
}

/*
 * KnownAssignedXidsGet - 通过扫描 KnownAssignedXids 获取一个 xids 数组。
 * 我们过滤掉任何 >= xmax 的条目。
 *
 * 返回存储到 xarray[] 中的 XID 数量。调用者负责
 * 确保数组足够大。
 *
 * 调用者必须以（至少）共享模式持有 ProcArrayLock。
 */
static int fc_KnownAssignedXidsGet(TransactionId *fc_xarray, TransactionId fc_xmax)
{
	TransactionId fc_xtmp = InvalidTransactionId;

	return fc_KnownAssignedXidsGetAndSetXmin(fc_xarray, &fc_xtmp, fc_xmax);
}

/*
 * KnownAssignedXidsGetAndSetXmin - 如同 KnownAssignedXidsGet，
 * 加上如果 xmin 尚未更低，则将 *xmin 降至所见的最低 xid 值。
 *
 * 调用者必须以（至少）共享模式持有 ProcArrayLock。
 */
static int fc_KnownAssignedXidsGetAndSetXmin(TransactionId *fc_xarray, TransactionId *fc_xmin,
							   TransactionId fc_xmax)
{
	int			fc_count = 0;
	int			fc_head,
				fc_tail;
	int			fc_i;

	/*
	 * 仅需一次获取头部，因为在循环中可能会改变。一旦我们达到最初看到的头部，
	 * 就可以停止，因为我们可以确定一个 xid 在持有 ProcArrayLock 的情况下
	 * 不会进入然后离开数组。我们可能会错过新添加的 xids，但它们应该是 >= xmax，
	 * 所以无关紧要。
	 *
	 * 必须采取自旋锁以确保我们看到最新的数组内容。
	 */
	SpinLockAcquire(&procArray->known_assigned_xids_lck);
	fc_tail = procArray->tailKnownAssignedXids;
	fc_head = procArray->headKnownAssignedXids;
	SpinLockRelease(&procArray->known_assigned_xids_lck);

	for (fc_i = fc_tail; fc_i < fc_head; fc_i++)
	{
		/* 跳过数组中的任何空隙 */
		if (KnownAssignedXidsValid[fc_i])
		{
			TransactionId fc_knownXid = KnownAssignedXids[fc_i];

			/*
			 * 如果需要，更新 xmin。只需检查第一个 XID，
			 * 因为数组是排序的。
			 */
			if (fc_count == 0 &&
				TransactionIdPrecedes(fc_knownXid, *fc_xmin))
				*fc_xmin = fc_knownXid;

			/*
			 * 再次过滤掉任何 >= xmax 的条目，依赖于数组的排序特性。
			 */
			if (TransactionIdIsValid(fc_xmax) &&
				TransactionIdFollowsOrEquals(fc_knownXid, fc_xmax))
				break;

			/* 将 knownXid 添加到输出数组 */
			fc_xarray[fc_count++] = fc_knownXid;
		}
	}

	return fc_count;
}

/*
 * 获取 KnownAssignedXids 数组中的最旧 XID，如果没有则返回 InvalidTransactionId。
 */
static TransactionId fc_KnownAssignedXidsGetOldestXmin(void)
{
	int			fc_head,
				fc_tail;
	int			fc_i;

	/*
	 * 仅需一次获取头部，因为在循环中可能会改变。
	 */
	SpinLockAcquire(&procArray->known_assigned_xids_lck);
	fc_tail = procArray->tailKnownAssignedXids;
	fc_head = procArray->headKnownAssignedXids;
	SpinLockRelease(&procArray->known_assigned_xids_lck);

	for (fc_i = fc_tail; fc_i < fc_head; fc_i++)
	{
		/* 跳过数组中的任何空隙 */
		if (KnownAssignedXidsValid[fc_i])
			return KnownAssignedXids[fc_i];
	}

	return InvalidTransactionId;
}

/*
 * 显示 KnownAssignedXids 以提供调试痕迹
 *
 * 目前这仅在启动过程内调用，因此我们不需要
 * 特别的锁定。
 *
 * 注意，这相当昂贵，即使 elog 消息会被丢弃，
 * 很大一部分开销也将被承担。然而，它目前没有在任何
 * 性能关键的地方被调用，因此不需要过于紧张。
 */
static void fc_KnownAssignedXidsDisplay(int fc_trace_level)
{
	ProcArrayStruct *fc_pArray = procArray;
	StringInfoData fc_buf;
	int			fc_head,
				fc_tail,
				fc_i;
	int			fc_nxids = 0;

	fc_tail = fc_pArray->tailKnownAssignedXids;
	fc_head = fc_pArray->headKnownAssignedXids;

	initStringInfo(&fc_buf);

	for (fc_i = fc_tail; fc_i < fc_head; fc_i++)
	{
		if (KnownAssignedXidsValid[fc_i])
		{
			fc_nxids++;
			appendStringInfo(&fc_buf, "[%d]=%u ", fc_i, KnownAssignedXids[fc_i]);
		}
	}

	elog(fc_trace_level, "%d KnownAssignedXids (num=%d tail=%d head=%d) %s",
		 fc_nxids,
		 fc_pArray->numKnownAssignedXids,
		 fc_pArray->tailKnownAssignedXids,
		 fc_pArray->headKnownAssignedXids,
		 fc_buf.data);

	pfree(fc_buf.data);
}

/*
 * KnownAssignedXidsReset
 *		重置 KnownAssignedXids 为为空
 */
static void fc_KnownAssignedXidsReset(void)
{
	ProcArrayStruct *fc_pArray = procArray;

	LWLockAcquire(ProcArrayLock, LW_EXCLUSIVE);

	fc_pArray->numKnownAssignedXids = 0;
	fc_pArray->tailKnownAssignedXids = 0;
	fc_pArray->headKnownAssignedXids = 0;

	LWLockRelease(ProcArrayLock);
}
